Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4e6dd33
Add FastAPI + htmx web frontend for all tools
gaurav Feb 16, 2026
b50daf0
Added a list of NodeNorm URLs.
gaurav Feb 16, 2026
bdf9d19
Replace NodeNorm URL text input with dropdown from NodeNorm.URLs
gaurav Feb 16, 2026
711a4ad
Fix --reload by passing app factory as import string to uvicorn
gaurav Feb 16, 2026
92a21ea
Fixed description display in web.
gaurav Feb 16, 2026
11510a0
Add API Docs and GitHub links to navbar
gaurav Feb 16, 2026
cd3bb6d
Document web frontend in CLAUDE.md and README.md
gaurav Feb 16, 2026
27ec599
Merge branch 'basic-implementation-in-uv' into add-nodenorm-frontend
gaurav Mar 26, 2026
0bcdc48
Document frontend deployment architecture: split by data dependency
gaurav Mar 26, 2026
801ceed
Scaffold Astro + Vue web frontend and shared endpoint config
gaurav Mar 26, 2026
5cba309
Add layout, navbar, landing page, types, and CURIE link utilities
gaurav Mar 26, 2026
5df2958
Implement NodeNorm single-instance lookup (Feature 1)
gaurav Mar 26, 2026
df0b175
Add summary card showing aggregate stats across all CURIEs
gaurav Mar 26, 2026
2925362
Add multi-instance comparison mode (Feature 2)
gaurav Mar 26, 2026
51d32b6
Add FUTURE.md and fix base URL trailing slash in navigation
gaurav Mar 26, 2026
78890cf
Update CLAUDE.md, README.md, and web/README.md for dual-frontend arch…
gaurav Mar 26, 2026
e1d03f2
Fix expand/recurse param bug and deduplicate route helpers
gaurav Mar 27, 2026
60a09c9
Reformatted code with `ruff`.
gaurav Mar 28, 2026
a5e3f27
Add Vitest tests for Astro/Vue frontend and fix description type bug
gaurav Mar 28, 2026
8b934ea
Fix description field types and expand test fixtures for conflation
gaurav Mar 28, 2026
8d86ca8
Rename npm package from "web" to "babel-explorer"
gaurav Mar 28, 2026
e6fea59
Update CLAUDE.md, web/README.md, web/tests/README.md with recent changes
gaurav Mar 28, 2026
08ca86e
Add URL state persistence and Share button to NodeNorm page
gaurav Mar 28, 2026
c5d9085
Update docs to reflect URL state persistence feature (77 tests)
gaurav Mar 28, 2026
2f7bf0a
Unify NodeNorm UI: remove mode toggle, add expandable table rows
gaurav Mar 28, 2026
e443ae5
Replace per-instance SummaryCards with unified ResultsSummary component
gaurav Mar 28, 2026
c9f9eb9
Update docs to reflect unified NodeNorm UI (99 tests)
gaurav Mar 28, 2026
d97572d
Merge branch 'basic-implementation-in-uv' into add-nodenorm-frontend
gaurav Mar 30, 2026
4bfd9cb
Merge branch 'basic-implementation-in-uv' into add-nodenorm-frontend
gaurav Apr 1, 2026
9b383a1
Fix hashing crash, cache loss, stale hasResults, and TOCTOU races
gaurav Apr 1, 2026
4374a09
Separate query/results panes into cards; add JSON export; collapse ad…
gaurav Apr 1, 2026
a3b24ea
Fix Show Columns toggle: gate type badges in main row on visibleColumns
gaurav Apr 1, 2026
09f2a67
Make Types tile an interactive filter using most-specific type only
gaurav Apr 1, 2026
b8123f7
Renamed "equiv. IDs" to "equivalent IDs".
gaurav Apr 2, 2026
10eacdd
Fixed missed equiv ID.
gaurav Apr 2, 2026
0ca7d10
Show taxa in collapsed card header when Taxa toggle is on
gaurav Apr 2, 2026
6034d1a
Fix query pane not reflecting URL params on shared link open
gaurav Apr 2, 2026
5e2e273
Add GitHub Actions workflow to deploy web/ to GitHub Pages
gaurav Apr 2, 2026
c622ca5
Merge branch 'basic-implementation-in-uv' into add-nodenorm-frontend
gaurav Apr 2, 2026
bc6aae5
Activate on all pull requests.
gaurav Apr 2, 2026
d472d9c
Cleaned up GitHub Pages deploy triggers.
gaurav Apr 2, 2026
6407404
Display direct biolink types (from individual_types) as linked badges
gaurav Apr 3, 2026
d68b936
Fix ComparisonView to use direct types for display and filtering
gaurav Apr 3, 2026
bdadff5
Add raw API link to expanded NodeNorm result panels
gaurav Apr 6, 2026
df00ecc
Updated package-lock.json.
gaurav Apr 6, 2026
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
39 changes: 39 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Deploy to GitHub Pages

on:
# New releases will cause the gh-pages branch to be updated.
release:
# TODO: Remove the pull_request trigger below before merging this PR
pull_request:

permissions:
contents: write

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: web

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

- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: web/dist
# force_orphan replaces the entire gh-pages branch on every deploy,
# ensuring no previously-deployed files are left behind.
force_orphan: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ cython_debug/
# you could uncomment the following to ignore the entire vscode folder
# .vscode/

# Node.js (web/ frontend)
node_modules/
web/dist/

# Ruff stuff:
.ruff_cache/

Expand Down
117 changes: 108 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ babel-explorer is a tool for querying and exploring Babel intermediate files. It

## Development Setup

This project uses **uv** for package management:
This project has two package managers — **uv** for Python and **npm** for the Astro/Vue frontend:

```bash
# Install dependencies
uv sync

# Install with dev dependencies
# Python (CLI + server frontend)
uv sync --group dev

# Run the CLI
uv run babel-explorer --help

# Astro/Vue frontend (GitHub Pages)
cd web && npm install && npm run dev
```

## Commands
Expand All @@ -43,6 +41,12 @@ uv run babel-explorer test-concord MONDO:0004979 HP:0000001

# Use custom Babel server or local directory
uv run babel-explorer xrefs MONDO:0004979 --local-dir data/2025nov19 --babel-url https://stars.renci.org:443/var/babel_outputs/2025nov19/

# Start the web server
uv run babel-explorer web

# Start with custom options
uv run babel-explorer web --host 0.0.0.0 --port 9000 --reload
```

### Development Commands
Expand All @@ -65,6 +69,12 @@ uv run ruff check

# Format code
uv run ruff format

# Astro/Vue frontend (from web/ directory)
cd web && npm run dev # Dev server at localhost:4321
cd web && npm run build # Build to web/dist/
cd web && npm test # Run Vitest unit + component tests
cd web && npm run test:watch # Watch mode
```

## Architecture
Expand All @@ -87,11 +97,36 @@ uv run ruff format
- Integration with NodeNormalization API (https://nodenormalization-sri.renci.org/)
- Fetches labels, biolink types, and equivalent identifiers for CURIEs
- Uses `@functools.lru_cache` for performance
- `NodeNorm.URLs` loaded from `config/translator-endpoints.json` (shared with the Astro frontend)
- Optional component for label enrichment

4. **CLI** (`src/babel_explorer/cli.py`):
- Click-based command-line interface
- Three main commands: `xrefs`, `ids`, `test-concord`
- Four main commands: `xrefs`, `ids`, `test-concord`, `web`

5. **Python Web Frontend** (`src/babel_explorer/web/`):
- FastAPI + Jinja2 + htmx + Bootstrap 5 (CDN) web interface
- App factory in `web/__init__.py` — `create_app(local_dir, babel_url, nodenorm_url)`
- All routes in `web/routes.py` — HTML pages, htmx partials, JSON API, CSV downloads
- Templates in `web/templates/` with `_partials/` for htmx fragments
- Sync route handlers (core code is synchronous; FastAPI runs them in a threadpool)
- Four tools exposed: NodeNorm, XRefs, IDs, Test Concordance
- Deployed to Kubernetes (hosts DB-dependent tools)

6. **Astro/Vue Web Frontend** (`web/`):
- Astro + Vue 3 static site for browser-only tools (no backend required)
- npm package name: `babel-explorer` (directory stays `web/`)
- Deployed to GitHub Pages
- Currently hosts: NodeNorm lookup (unified instance selection via checkboxes, expandable result table, shareable URLs)
- Calls NodeNorm API directly from the browser via `fetch()` (CORS-enabled)
- Uses Bootstrap 5 (CDN) with the same dark-navbar styling as the Python frontend
- CURIE link-outs via [biolink-model prefix map](https://github.com/biolink/biolink-model) (v4.3.7, fetched at runtime)
- Tested with Vitest + @vue/test-utils + happy-dom (99 tests); see `web/tests/README.md`
- See `web/README.md` for development instructions and `web/FUTURE.md` for deferred features

7. **Shared Configuration** (`config/`):
- `config/translator-endpoints.json` — single source of truth for NodeNorm and NameRes deployment URLs across all environments (dev, exp, ci, test, prod)
- Consumed by Python CLI/frontend (`nodenorm.py`) and Astro frontend (`NodeNormApp.vue`)

### Data Flow

Expand All @@ -100,13 +135,66 @@ uv run ruff format
3. BabelXRefs queries files using DuckDB
4. If `--labels` or `--recurse` flags are set, NodeNorm is queried for additional metadata
5. Results are printed to stdout
1. User provides CURIEs via CLI, Python web UI, or Astro web UI
2. For DB-dependent tools (XRefs, IDs): BabelDownloader ensures required Parquet files are downloaded, BabelXRefs queries files using DuckDB
3. For API-only tools (NodeNorm): the Astro frontend calls the NodeNorm API directly from the browser; the Python frontend proxies through the server
4. If `--labels` or `--expand` flags are set, NodeNorm is queried for additional metadata
5. Results are printed to stdout (CLI), rendered as HTML tables / JSON / CSV (Python web), or rendered as Vue components (Astro web)

### Astro/Vue Frontend Structure (`web/`)

```
web/src/
layouts/BaseLayout.astro # Bootstrap 5 CDN + dark navbar
components/
Navbar.astro # Dark navbar matching Python frontend
nodenorm/
NodeNormApp.vue # Root Vue island (client:only="vue")
NodeNormForm.vue # Form: textarea, checkboxes + custom URL, API toggles
ComparisonView.vue # Results table (rows=CURIEs, cols=instances) with expandable rows
CurieDetailPanel.vue # Expandable detail body: description, types, IC, equiv IDs
CurieResultCard.vue # Accordion card wrapping CurieDetailPanel (single-instance use)
ResultsSummary.vue # Unified stat tiles: normalized count, disagreements, types
EquivalentIdTable.vue # Striped table with togglable columns
ColumnVisibility.vue # Page-wide column show/hide controls
shared/
CurieLink.vue # CURIE → external URL link using biolink prefix map
lib/
nodenorm-api.ts # fetch() wrapper for NodeNorm get_normalized_nodes (AbortSignal support)
curie-links.ts # Biolink prefix map loader + URL builder
url-state.ts # Encode/decode query state in URL params (readQueryState, buildQueryUrl)
types.ts # TypeScript interfaces + DEFAULT_API_OPTIONS
pages/
index.astro # Landing page with tool cards
nodenorm.astro # NodeNorm tool page
```

Key patterns:
- Each tool page hosts one Vue island via `client:only="vue"` (no SSR — all client-side)
- Shared config imported from `config/translator-endpoints.json` at build time
- Bootstrap via CDN (not npm) to match the Python frontend exactly

### Python Web Route Structure

| Route | Method | Returns | Purpose |
|-------|--------|---------|---------|
| `/` | GET | HTML | Landing page |
| `/nodenorm`, `/xrefs`, `/ids`, `/test-concord` | GET | HTML | Tool form pages |
| `/htmx/nodenorm`, `/htmx/xrefs`, `/htmx/ids`, `/htmx/test-concord` | POST | HTML partial | htmx result fragments |
| `/api/nodenorm`, `/api/xrefs`, `/api/ids`, `/api/test-concord` | GET | JSON | REST API (`?curie=X&curie=Y`) |
| `/api/*/csv` | GET | CSV file | CSV download for each tool |
| `/docs` | GET | HTML | Swagger UI (auto-generated by FastAPI) |

The NodeNorm and Test Concordance pages include a dropdown to select from predefined `NodeNorm.URLs` instances or enter a custom URL.

### Key Design Patterns

- **Lazy downloading**: Files are only downloaded when first accessed
- **LRU caching**: Heavy use of `@functools.lru_cache` to avoid redundant downloads and API calls
- **Recursive expansion**: The `--recurse` flag recursively follows all cross-references to build complete graphs
- **DuckDB for querying**: In-memory SQL queries against Parquet files for fast lookups
- **Dual frontend**: DB-dependent tools on a server (FastAPI + htmx), API-only tools in the browser (Astro + Vue). Split follows data dependencies.
- **Shared config**: `config/translator-endpoints.json` is the single source of truth for deployment URLs, consumed by both frontends and the CLI

## Testing

Expand All @@ -123,12 +211,14 @@ Tests live in `tests/` and are split into fast **unit tests** (mocked, no networ
| `tests/test_babel_xrefs.py` | 23 | 20 | 3 | 46 |
| `tests/test_nodenorm.py` | 20 | 13 | 0 | 33 |
| `tests/test_cli.py` | 24 | 0 | 0 | 24 |
| `tests/test_web.py` | 25 | 0 | 0 | 25 |

### Test Infrastructure

- **`tests/conftest.py`** — Session-scoped fixtures that download Parquet files once and share them across all integration tests. Teardown removes the `data/test/` directory so the next run starts fresh.
- **`tests/constants.py`** — Shared constants (URLs, file paths) and `load_curies()` helper.
- **`tests/data/valid_curies.txt`** — One CURIE per line (`#` comments allowed). Integration tests are parametrized over this list — adding a new line automatically expands test coverage.
- **`tests/fixtures/`** — JSON snapshots of real NodeNorm API responses, shared by both Python and TypeScript test suites. Includes single-CURIE responses, batch (multi-CURIE) responses, conflated vs. non-conflated variants, and a biolink prefix map subset. See `web/tests/README.md` for the full fixture list and how to regenerate them.

### Key Dataclasses

Expand All @@ -144,8 +234,17 @@ Tests live in `tests/` and are split into fast **unit tests** (mocked, no networ

## File Locations

- Source code: `src/babel_explorer/`
- Python source code: `src/babel_explorer/`
- Tests: `tests/`
- Test CURIEs: `tests/data/valid_curies.txt`
- Downloaded Babel files: `data/<version>/duckdb/*.parquet`
- Generated DuckDB databases: `data/<version>/output/duckdbs/`
- Python web frontend: `src/babel_explorer/web/`
- Python web templates: `src/babel_explorer/web/templates/`
- Astro/Vue web frontend: `web/`
- Astro/Vue components: `web/src/components/`
- Astro/Vue pages: `web/src/pages/`
- Astro/Vue tests: `web/src/**/__tests__/` (co-located) + `web/tests/README.md`
- Shared config: `config/translator-endpoints.json`
- Shared test fixtures (Python + TS): `tests/fixtures/`
- Entry point: `src/babel_explorer/cli.py`
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,92 @@ uv run babel-explorer ids MONDO:0004979
uv run babel-explorer test-concord MONDO:0004979 HP:0000001
```

## Web Frontends

babel-explorer has **two** web frontends that share the same Bootstrap 5 dark-navbar styling. See [Architecture: Frontend Deployment](#architecture-frontend-deployment) for the rationale behind the split.

### Astro/Vue Frontend (GitHub Pages)

Static site built with Astro + Vue 3. Hosts API-only tools that run entirely in the browser.

```bash
cd web
npm install
npm run dev # Dev server at http://localhost:4321
npm run build # Build to web/dist/
```

**Currently available:** NodeNorm lookup (single-instance and multi-instance comparison).

### Python Frontend (Kubernetes)

FastAPI + htmx server. Hosts database-dependent tools that need DuckDB and multi-GB Parquet files.

```bash
# Start the web server (default: http://127.0.0.1:8000)
uv run babel-explorer web

# Custom host/port
uv run babel-explorer web --host 0.0.0.0 --port 9000

# Auto-reload for development
uv run babel-explorer web --reload
```

Once running, open http://127.0.0.1:8000 in a browser. The navbar links to each tool page, the Swagger API docs (`/docs`), and this GitHub repository.

### REST API

The JSON API accepts CURIEs as repeated query parameters:

```bash
# NodeNorm lookup
curl "http://127.0.0.1:8000/api/nodenorm?curie=MONDO:0004979"

# Cross-references with expansion
curl "http://127.0.0.1:8000/api/xrefs?curie=MONDO:0004979&expand=true"

# Identifier records
curl "http://127.0.0.1:8000/api/ids?curie=MONDO:0004979"

# Test concordance
curl "http://127.0.0.1:8000/api/test-concord?curie=MONDO:0004979"
```

CSV downloads are available at `/api/nodenorm/csv`, `/api/xrefs/csv`, `/api/ids/csv`, and `/api/test-concord/csv` with the same query parameters.

## Architecture: Frontend Deployment

babel-explorer's four web tools have fundamentally different infrastructure needs:

| Tool | Data dependency | Needs a server? |
|------|----------------|-----------------|
| **XRefs** | Concord.parquet (~626 MB) + DuckDB | Yes |
| **IDs** | Identifiers.parquet (~2 GB+) + DuckDB | Yes |
| **NodeNorm** | External API only (CORS-enabled) | No |
| **Test Concordance** | External API only (CORS-enabled) | No |

XRefs and IDs query multi-GB Parquet files via DuckDB — they genuinely need a server with those files on disk. NodeNorm and Test Concordance are pure API proxies: the server adds latency and a failure point for zero value, since the NodeNorm API supports CORS and can be called directly from the browser.

The frontend is therefore split along this natural dependency boundary:

### Server (Kubernetes) — `src/babel_explorer/web/`

The FastAPI + htmx server hosts **XRefs** and **IDs**. It downloads and caches Parquet files, runs DuckDB queries, and renders HTML server-side. It also exposes the JSON REST API (`/api/xrefs`, `/api/ids`) and CSV downloads for programmatic access.

### GitHub Pages (static) — `web/`

**NodeNorm** (and eventually **Test Concordance**) are built as an Astro + Vue 3 static site in the `web/` directory. These pages call the NodeNorm API directly from the browser using `fetch()` — no server required.

Deployment URLs for NodeNorm and NameRes are defined once in `config/translator-endpoints.json`, shared by both frontends and the CLI. CURIE link-outs use the [biolink-model prefix map](https://github.com/biolink/biolink-model) (fetched at runtime). Both sites share Bootstrap 5 dark-navbar styling for a consistent look.

### Why this split?

- **Preserves htmx where it matters.** XRefs/IDs benefit from server-side rendering because the data is already on the server. No need to rewrite them in client-side JS.
- **Minimal new code.** The JS for calling NodeNorm directly is trivial (~200 lines). A fully headless API approach would require replicating htmx partial rendering for XRefs/IDs dynamic columns in JS — more code for no gain.
- **Resilience.** If the Kubernetes pod is down or a Parquet download stalls, the API-only tools still work on GitHub Pages.
- **Reusable pattern.** Any CORS-enabled Translator API (NameRes, Node Annotator, etc.) can get a GitHub Pages demo the same way. This demonstrates to the [translator_sdk](https://github.com/NCATSTranslator/Translator_sdk) team that web UIs for API tools are easy and free to host, without complicating that repo's PyPI package. translator_sdk stays as a Python SDK (pipx-runnable CLI); web demos can live in `docs/` here or in a shared repo if the pattern scales.

## Testing

Tests are split into fast **unit tests** (mocked, no network) and slower **integration tests** (real file downloads and API calls), controlled by pytest markers.
Expand Down
16 changes: 16 additions & 0 deletions config/translator-endpoints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"nodenorm": {
"dev": "https://nodenormalization-sri.renci.org/",
"exp": "https://nodenormalization-exp.apps.renci.org/",
"ci": "https://nodenorm.ci.transltr.io/",
"test": "https://nodenorm.test.transltr.io/",
"prod": "https://nodenorm.transltr.io/"
},
"nameres": {
"dev": "https://name-resolution-sri.renci.org/",
"exp": "https://name-resolution-exp.apps.renci.org/",
"ci": "https://name-lookup.ci.transltr.io/",
"test": "https://name-lookup.test.transltr.io/",
"prod": "https://name-lookup.transltr.io/"
}
}
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ requires-python = ">=3.11"
dependencies = [
"click>=8.3.1",
"duckdb>=1.4.2",
"fastapi>=0.115.0",
"jinja2>=3.1.0",
"python-multipart>=0.0.9",
"requests>=2.32.5",
"tqdm>=4.67.0",
"uvicorn[standard]>=0.30.0",
]

[build-system]
Expand All @@ -17,6 +21,7 @@ build-backend = "hatchling.build"

[dependency-groups]
dev = [
"httpx>=0.27.0",
"filelock>=3.16",
"pytest>=8.3.5",
"pytest-xdist[psutil]>=3.6",
Expand Down
Loading
Loading