From 46447d1f3fd71fb54815ab883f7e89816d613de5 Mon Sep 17 00:00:00 2001 From: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:38:24 +0530 Subject: [PATCH] fix(explorer): resolve blank dashboard UI and ship frontend bundle in wheel (#638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(explorer): resolve blank dashboard UI and ship frontend bundle in wheel Fixes #631 — the Explorer server started successfully but the browser showed a blank page because semantica/static/ was gitignored and never present after a fresh install or clone. Changes: - ci.yml / release.yml: add Node 20 setup + npm ci && npm run build before python -m build so every wheel contains a CI-built frontend bundle - pyproject.toml: add package-data patterns (static/*, static/assets/*) so setuptools includes the bundle in the wheel; add MANIFEST.in for sdist coverage - app.py: replace silent empty-HTML fallback with a 200 page that clearly explains the missing bundle and links to /docs; fix CORS allow_credentials to default false, gated behind EXPLORER_CORS_CREDENTIALS env var to prevent credentialed cross-origin requests on unauthenticated endpoints - __init__.py: warn at startup when --host is non-loopback (unauthenticated network exposure) - explorer/README.md: full rewrite covering pip-install mode (primary path, no Node required) and dev-server mode (contributors), CLI flags, env vars, workspace table, troubleshooting for the blank-page symptom - README.md: update Knowledge Explorer section with correct command and link to the new setup guide * fix(explorer): set build.target esnext to fix esbuild CI failure esbuild >=0.28 (forced via npm overrides) conflicts with Vite 6 defaults on Linux CI — it tries to lower destructuring syntax for the implicit browser target list but errors out. Explicit target: 'esnext' tells esbuild to emit native syntax unchanged, bypassing the transpilation error entirely. Safe for a developer tool that runs in modern browsers. * test(explorer): verify packaged frontend bundle --------- Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> --- .github/workflows/ci.yml | 27 +++ .github/workflows/release.yml | 27 +++ MANIFEST.in | 1 + README.md | 11 +- explorer/README.md | 262 ++++++++++++++++++---------- explorer/vite.config.ts | 4 + pyproject.toml | 5 + semantica/explorer/__init__.py | 10 ++ semantica/explorer/app.py | 28 ++- tests/explorer/test_explorer_api.py | 48 ++++- 10 files changed, 323 insertions(+), 100 deletions(-) create mode 100644 MANIFEST.in diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53f95c1..b5c0d804 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,5 +22,32 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: explorer/package-lock.json + - name: Build Explorer frontend + working-directory: explorer + run: | + npm ci + npm run build - run: pip install build - run: python -m build + - name: Verify Explorer frontend is packaged + run: | + python - <<'PY' + import zipfile + from pathlib import Path + + wheels = list(Path("dist").glob("*.whl")) + assert wheels, "No wheel was built" + + with zipfile.ZipFile(wheels[0]) as wheel: + names = set(wheel.namelist()) + + assert "semantica/static/index.html" in names, "Explorer index.html missing from wheel" + assert any(name.startswith("semantica/static/assets/") for name in names), "Explorer assets missing from wheel" + + print("Explorer frontend is packaged") + PY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b90f58cb..7e64badf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,35 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: explorer/package-lock.json + - name: Build Explorer frontend + working-directory: explorer + run: | + npm ci + npm run build - run: pip install build - run: python -m build + - name: Verify Explorer frontend is packaged + run: | + python - <<'PY' + import zipfile + from pathlib import Path + + wheels = list(Path("dist").glob("*.whl")) + assert wheels, "No wheel was built" + + with zipfile.ZipFile(wheels[0]) as wheel: + names = set(wheel.namelist()) + + assert "semantica/static/index.html" in names, "Explorer index.html missing from wheel" + assert any(name.startswith("semantica/static/assets/") for name in names), "Explorer assets missing from wheel" + + print("Explorer frontend is packaged") + PY - uses: softprops/action-gh-release@v3 with: files: dist/* diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..aa726d6b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include semantica/static * diff --git a/README.md b/README.md index f0b0e32c..c8eadda0 100644 --- a/README.md +++ b/README.md @@ -1369,12 +1369,17 @@ A browser-based graph workbench. Pan and zoom live graphs, scrub the timeline, r | **Ontology Hub** | SHACL Studio, visual editor, cross-ontology alignments, SKOS browser | | **Lineage** | W3C PROV-O provenance visualization for any entity | +Quickest way to start (no Node.js required): + ```bash -python -m semantica.server # backend on port 8000 -cd explorer && npm install && npm run dev # UI on port 5173 +pip install "semantica[explorer]" +semantica-explorer --graph my_graph.json +# Dashboard opens at http://127.0.0.1:8000 ``` -→ [`explorer/README.md`](explorer/README.md) +For contributor / dev-server setup, see the full local setup guide: + +→ **[explorer/README.md — Local Setup Guide](explorer/README.md)** --- diff --git a/explorer/README.md b/explorer/README.md index 2bb6b2c4..3884aaee 100644 --- a/explorer/README.md +++ b/explorer/README.md @@ -1,116 +1,153 @@ # Semantica Knowledge Explorer -A real-time visual interface for exploring knowledge graphs, decision intelligence, entity resolution, ontologies, and graph analytics built on top of the [Semantica](https://github.com/Hawksight-AI/semantica) library. +A browser-based graph workbench for the [Semantica](https://github.com/semantica-agi/semantica) platform. Pan and zoom live graphs, scrub the timeline, trace every decision's causal chain, resolve duplicates, and author your ontology visually. Built on React 19 + Sigma.js. --- ## Requirements -| Dependency | Minimum Version | -|---|---| +| Dependency | Minimum version | +| --- | --- | +| Python | 3.8+ | | Node.js | 18.x or higher (20.x recommended) | | npm | 9.x or higher | -| Python | 3.8+ | -| Semantica backend | running on `http://127.0.0.1:8000` | - -Check your versions: ```bash +python --version node --version npm --version -python --version ``` --- -## Quick Start (Local Development) +## Two ways to run the Explorer -### 1. Clone the repository +### Option A — pip install (recommended for users) + +Install the package with the explorer extras. The pre-built frontend bundle is included in the wheel so no Node.js is required. ```bash -git clone https://github.com/Hawksight-AI/semantica.git +pip install "semantica[explorer]" +``` + +Launch the dashboard by pointing it at any graph JSON file: + +```bash +semantica-explorer --graph my_graph.json +``` + +The server starts at `http://127.0.0.1:8000` and opens the dashboard in your default browser automatically. + +CLI flags: + +| Flag | Default | Description | +| --- | --- | --- | +| `--graph` / `-g` | *(required)* | Path to a ContextGraph JSON file | +| `--port` / `-p` | `8000` | Port to bind the server to | +| `--host` | `127.0.0.1` | Host to bind (use `127.0.0.1` for local-only; see security note below) | +| `--no-browser` | off | Skip opening the browser automatically | + +Examples: + +```bash +# Default — opens at http://127.0.0.1:8000 +semantica-explorer --graph my_graph.json + +# Custom port +semantica-explorer --graph my_graph.json --port 8080 + +# Suppress auto-open +semantica-explorer --graph my_graph.json --no-browser + +# Equivalent using python -m +python -m semantica.explorer --graph my_graph.json +``` + +> **Security note:** The Explorer API has no built-in authentication. The default `--host 127.0.0.1` binds to localhost only, so it is not reachable from other machines on your network. If you bind to `0.0.0.0`, all graph data is readable and writable by any host that can reach the port. The CLI will print a warning in that case. + +--- + +### Option B — run from source (for contributors / frontend development) + +This mode runs the React dev server with hot module replacement, so frontend changes appear in the browser instantly without rebuilding. + +#### Step 1 — Clone the repo + +```bash +git clone https://github.com/semantica-agi/semantica.git cd semantica ``` -### 2. Install the Semantica Python package +#### Step 2 — Install the Python package ```bash -pip install semantica +pip install -e ".[explorer]" ``` -Or install from source if you have the repo: - -```bash -pip install -e . -``` - -### 3. Start the Semantica backend - -The Explorer proxies all `/api` and `/ws` requests to `http://127.0.0.1:8000`. The backend must be running before you open the UI. - -```bash -# From the repo root -python -m semantica.server -``` - -The backend starts on port **8000** by default. Keep this terminal open. - -### 4. Install frontend dependencies - -Open a second terminal: +#### Step 3 — Install frontend dependencies ```bash cd explorer -npm install +npm ci ``` -> **Note:** This project uses Vite 5 and requires **Node 18+**. If you are on Node 16 or earlier, upgrade first. +#### Step 4 — Start the Python backend -### 5. Start the dev server +Open a terminal in the repo root: + +```bash +semantica-explorer --graph path/to/my_graph.json --no-browser +``` + +This starts the API on `http://127.0.0.1:8000`. Keep this terminal open. + +#### Step 5 — Start the frontend dev server + +Open a second terminal in `explorer/`: ```bash npm run dev ``` -Vite starts on **http://localhost:5173** by default. Open that URL in your browser. +Vite starts on **`http://localhost:5173`**. Open that URL in your browser. All `/api` and `/ws` requests are automatically proxied to the Python backend at `http://127.0.0.1:8000`. --- -## What you should see +## Building the production bundle -The Explorer opens with a persistent left sidebar and six workspace tabs: +If you need to serve the UI from the Python server directly (without the Vite dev server): -| Tab | What it shows | -|---|---| -| **Knowledge Graph** | Interactive Sigma.js canvas — nodes, edges, zoom, ForceAtlas2 layout | -| **Timeline** | Temporal event scrubber over the graph | -| **Decisions** | Causal chain viewer with outcome badges and decision filter | -| **Registry** | Live audit log of every graph mutation (add-node, add-edge, etc.) | -| **Entity Resolution** | Duplicate detection and entity merge workflow | +```bash +cd explorer +npm ci +npm run build +``` + +This writes the compiled assets to `../semantica/static/`. The Python server then serves the full dashboard at `http://127.0.0.1:8000` — no separate Vite process needed. + +--- + +## Workspaces + +| Workspace | What you can do | +| --- | --- | +| **Knowledge Graph** | Live Sigma.js canvas · ForceAtlas2 layout · Ego Mode · semantic distance heatmap · path highlighting | +| **Timeline** | Temporal event scrubber — watch the graph evolve across time | +| **Decisions** | Browse causal chains behind every recorded decision with outcome badges and confidence scores | +| **Registry** | Live audit log of every graph mutation (add-node, add-edge, delete, update) | +| **Entity Resolution** | Review and merge duplicate entities with blocking + semantic dedup | | **KG Overview** | Aggregate stats, community breakdown, centrality heatmap | -| **Ontology** | SKOS/OWL vocabulary hierarchy and schema summary | +| **Ontology Hub** | SHACL Studio · visual drag-and-drop editor · cross-ontology alignments · SKOS browser | +| **Lineage** | W3C PROV-O provenance visualization for any entity | --- -## Project structure +## Environment variables -``` -explorer/ -├── src/ -│ ├── App.tsx # Root layout, tab routing, workspace wiring -│ ├── index.css # Global resets, fonts, keyframe animations -│ ├── store/ -│ │ └── registryStore.ts # Pub/sub audit registry (no external state lib) -│ └── workspaces/ -│ ├── GraphWorkspace/ # Sigma.js graph canvas + inspector panel -│ ├── DecisionWorkspace/ # Causal flow diagram + decision list -│ ├── TimelineWorkspace/ # vis-timeline temporal scrubber -│ ├── ManageWorkspace/ # Registry, KG Overview, Ontology tabs -│ └── EnrichWorkspace/ # Entity resolution tab -├── index.html -├── vite.config.ts # Dev proxy → 127.0.0.1:8000, build → ../semantica/static -└── package.json -``` +| Variable | Default | Description | +| --- | --- | --- | +| `EXPLORER_CORS_ORIGINS` | `http://localhost:5173,http://127.0.0.1:5173` | Comma-separated list of allowed CORS origins | +| `EXPLORER_CORS_CREDENTIALS` | `false` | Set to `true` to allow credentialed cross-origin requests (only needed behind an authenticating reverse proxy) | --- @@ -122,7 +159,7 @@ Run these from inside the `explorer/` directory: # Start the dev server with hot module replacement npm run dev -# Type-check and build a production bundle into ../semantica/static +# Type-check and build the production bundle into ../semantica/static/ npm run build # Preview the production build locally @@ -133,65 +170,104 @@ npm run lint # Run the graph store multi-edge unit tests npm run test:graph-store + +# Run the graph workspace display tests +npm run test:graph-workspace ``` --- -## API & WebSocket proxy +## API & WebSocket proxy (dev mode only) During development, Vite forwards requests automatically — no CORS configuration needed: | Pattern | Forwarded to | -|---|---| +| --- | --- | | `/api/*` | `http://127.0.0.1:8000/api/*` | -| `/ws` | `ws://127.0.0.1:8000/ws` | +| `/ws/*` | `ws://127.0.0.1:8000/ws/*` | -If you run the backend on a different port, update `server.proxy` in [vite.config.ts](vite.config.ts). +To run the backend on a different port, update `server.proxy` in [vite.config.ts](vite.config.ts). --- -## Production build +## Project structure -```bash -cd explorer -npm run build +```text +explorer/ +├── src/ +│ ├── App.tsx # Root layout, tab routing, workspace wiring +│ ├── index.css # Global resets, fonts, keyframe animations +│ ├── store/ +│ │ ├── graphStore.ts # In-memory graph state +│ │ └── registryStore.ts # Pub/sub audit registry +│ └── workspaces/ +│ ├── GraphWorkspace/ # Sigma.js canvas + inspector + behaviors +│ ├── DecisionWorkspace/ # Causal flow diagram + decision list +│ ├── DiffMergeWorkspace/ # Graph diff and merge view +│ ├── EnrichWorkspace/ # Entity resolution + registry tabs +│ ├── ImportExportWorkspace/ # Import CSV/JSON, export graph +│ ├── LineageWorkspace/ # W3C PROV-O lineage diagram +│ ├── ManageWorkspace/ # KG Overview + Ontology Summary +│ ├── OntologyWorkspace/ # SHACL Studio, visual editor, SKOS browser +│ ├── SparqlWorkspace/ # In-browser SPARQL query editor +│ └── VocabularyWorkspace/ # SKOS vocabulary manager +├── index.html +├── vite.config.ts # Dev proxy → 127.0.0.1:8000, build → ../semantica/static +└── package.json ``` -The compiled assets are written to `../semantica/static/`. The Semantica Python server serves this folder automatically at its root URL — no separate web server needed. - --- ## Troubleshooting -**Blank graph / no data loads** -- Make sure the Semantica backend is running (`python -m semantica.server`) before opening the UI. -- Check the browser console for failed `/api/graph` requests — the proxy target may need updating in `vite.config.ts`. +### Dashboard shows a blank white page or "UI not available" message -**`npm install` fails or hangs** -- Ensure you are using **Node 18 or 20**. Node 16 and Vite 5 are incompatible. -- Delete `node_modules/` and `package-lock.json`, then re-run `npm install`. +The frontend bundle is missing from the server's static directory. Fix options: -**Port 5173 already in use** -- Vite will automatically try the next available port and print it in the terminal. Use that URL instead. +- **If you installed via pip:** `pip install --upgrade "semantica[explorer]"` — the wheel includes the pre-built bundle. +- **If you installed from source:** run `cd explorer && npm ci && npm run build` from the repo root, then restart the server. +- **In dev mode:** use the Vite dev server at `http://localhost:5173` instead of the backend URL. -**WebSocket not connecting (real-time mutations not appearing)** -- Confirm the backend exposes a `/ws` WebSocket endpoint. -- Check browser DevTools → Network → WS tab for the connection status. +### Blank graph / no data loads in the browser + +- Confirm the Python backend is running and check the terminal for errors. +- Open browser DevTools → Network tab and look for failed `/api/graph` requests. +- If the backend is on a different port, update `server.proxy` in `vite.config.ts`. + +### `npm ci` fails or reports missing lockfile + +The `package-lock.json` must be present. Run `npm install` once to generate it, commit it, then use `npm ci` going forward. + +### `npm run dev` fails with Node version error + +Vite 6 requires **Node 18 or higher**. Run `node --version` to check. If you're on Node 16, upgrade via [nvm](https://github.com/nvm-sh/nvm) or the official Node.js installer. + +### Port 5173 already in use + +Vite automatically tries the next available port and prints the actual URL in the terminal. Use the URL shown in the output. + +### WebSocket not connecting (real-time mutations not appearing) + +- Confirm the backend exposes the `/ws/graph-updates` WebSocket endpoint. +- Check DevTools → Network → WS tab for the connection status and error code. +- Ensure the backend version matches the frontend — mixing major versions can cause protocol mismatches. --- ## Tech stack -- **React 19** + TypeScript (strict `noUnusedLocals`) -- **Vite 5** with `babel-plugin-react-compiler` -- **Sigma.js 3** + **Graphology** — graph rendering and in-memory graph store -- **ForceAtlas2** — physics-based layout worker -- **@tanstack/react-query** — data fetching for ontology and vocab tabs +- **React 19** + TypeScript (strict mode) +- **Vite 6** with `babel-plugin-react-compiler` +- **Sigma.js 3** + **Graphology** — graph rendering and in-memory graph model +- **ForceAtlas2** — physics-based layout +- **@tanstack/react-query** — async data fetching for ontology and vocab tabs - **vis-timeline** — temporal event visualization +- **@xyflow/react** — lineage diagram rendering +- **Monaco Editor** — in-browser SPARQL / SHACL editor - **lucide-react** — icon set --- ## Contributing -See the root [CONTRIBUTING.md](../CONTRIBUTING.md) and open issues on the main [Semantica repository](https://github.com/Hawksight-AI/semantica). +See the root [CONTRIBUTING.md](../CONTRIBUTING.md) and open issues on the main [Semantica repository](https://github.com/semantica-agi/semantica). diff --git a/explorer/vite.config.ts b/explorer/vite.config.ts index 2f87b55c..0ada812a 100644 --- a/explorer/vite.config.ts +++ b/explorer/vite.config.ts @@ -17,6 +17,10 @@ export default defineConfig({ outDir: path.resolve(__dirname, '../semantica/static'), emptyOutDir: true, chunkSizeWarningLimit: 650, + // Explicit target avoids esbuild attempting to lower syntax that all + // modern browsers already support natively, which breaks with the + // esbuild >=0.28 override when running under Vite 6 on Linux CI. + target: 'esnext', rollupOptions: { output: { manualChunks(id) { diff --git a/pyproject.toml b/pyproject.toml index c1f8df1f..6f356151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -249,6 +249,11 @@ semantica-mcp = "semantica.mcp_server:main" where = ["."] include = ["semantica*", "integrations*"] +[tool.setuptools.package-data] +# Explicit patterns are more reliable than **/* across setuptools versions. +# static/* covers index.html / favicon; static/assets/* covers all JS/CSS chunks. +"semantica" = ["static/*", "static/assets/*"] + [tool.black] line-length = 88 diff --git a/semantica/explorer/__init__.py b/semantica/explorer/__init__.py index d89a0416..0445eb5f 100644 --- a/semantica/explorer/__init__.py +++ b/semantica/explorer/__init__.py @@ -80,6 +80,16 @@ def main(argv=None): app = create_app(session=session) url = f"http://{args.host}:{args.port}" + + _LOOPBACK_HOSTS = {"127.0.0.1", "::1", "localhost"} + if args.host not in _LOOPBACK_HOSTS: + _err.print( + f"[bold yellow]Warning:[/bold yellow] Binding to " + f"[cyan]{args.host}[/cyan] exposes the Explorer to the network. " + "The API has no authentication — all graph data will be readable " + "and writable by any host that can reach this port." + ) + if not args.no_browser: import threading threading.Timer(1.5, lambda: webbrowser.open(url)).start() diff --git a/semantica/explorer/app.py b/semantica/explorer/app.py index 47edb92d..e1242e5a 100644 --- a/semantica/explorer/app.py +++ b/semantica/explorer/app.py @@ -63,10 +63,16 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI: "EXPLORER_CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173" ) _cors_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()] + # allow_credentials lets browsers send cookies/auth headers cross-origin. + # The Explorer has no authentication, so credentials serve no purpose and + # enabling them when origins are broadened creates cross-site request risk. + # Set EXPLORER_CORS_CREDENTIALS=true explicitly to opt in (e.g. for a + # reverse-proxy setup that injects its own auth layer). + _allow_credentials = os.environ.get("EXPLORER_CORS_CREDENTIALS", "false").lower() == "true" app.add_middleware( CORSMiddleware, allow_origins=_cors_origins, - allow_credentials=True, + allow_credentials=_allow_credentials, allow_methods=["GET", "POST", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization"], max_age=600, @@ -139,10 +145,26 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI: index_path = Path(__file__).resolve().parent.parent / "static" / "index.html" if index_path.is_file(): return FileResponse(index_path) + _logger.warning( + "Explorer frontend bundle not found — UI unavailable. " + "Install the package via pip to get the pre-built bundle, " + "or run `cd explorer && npm ci && npm run build` from the repo root." + ) return HTMLResponse( '' - 'Semantica Knowledge Explorer' - '
' + 'Semantica Knowledge Explorer' + '' + "

Explorer UI not available

" + "

The frontend bundle was not found. This usually means the package was " + "installed from source without building the frontend first.

" + "

To fix: reinstall via " + "pip install semantica[explorer], or build from source with " + "cd explorer && npm ci && npm run build " + "then restart the server.

" + '

The REST API is still fully available at /docs.

' + "", + status_code=200, ) @app.get("/api/health") diff --git a/tests/explorer/test_explorer_api.py b/tests/explorer/test_explorer_api.py index e402ad79..dc1c4bb3 100644 --- a/tests/explorer/test_explorer_api.py +++ b/tests/explorer/test_explorer_api.py @@ -97,10 +97,56 @@ def client(): class TestHealthInfo: - def test_root_serves_spa(self, client): + def test_root_without_frontend_bundle_shows_diagnostic(self, tmp_path, monkeypatch, client): + from semantica.explorer import app as app_module + + package_dir = tmp_path / "semantica" + (package_dir / "explorer").mkdir(parents=True) + + class FakeAppPath: + def __init__(self, *_args, **_kwargs): + self.path = package_dir / "explorer" / "app.py" + + def resolve(self): + return self.path.resolve() + + monkeypatch.setattr(app_module, "Path", FakeAppPath) + response = client.get("/") + + assert response.status_code == 200 + assert "Explorer UI not available" in response.text + assert "frontend bundle was not found" in response.text + assert "/docs" in response.text + + def test_root_serves_built_spa_when_bundle_exists(self, tmp_path, monkeypatch): + from starlette.testclient import TestClient + from semantica.explorer import app as app_module + from semantica.explorer.app import create_app + + package_dir = tmp_path / "semantica" + static_dir = package_dir / "static" + static_dir.mkdir(parents=True) + (static_dir / "index.html").write_text( + '
', + encoding="utf-8", + ) + + class FakeAppPath: + def __init__(self, *_args, **_kwargs): + self.path = package_dir / "explorer" / "app.py" + + def resolve(self): + return self.path.resolve() + + monkeypatch.setattr(app_module, "Path", FakeAppPath) + + with TestClient(create_app()) as test_client: + response = test_client.get("/") + assert response.status_code == 200 assert '
' in response.text + assert '' in response.text def test_health(self, client): response = client.get("/api/health")