Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ npm-debug.log*
/.env*.local
/.env.development
/.env.production
backend/.env

# typescript
*.tsbuildinfo
Expand All @@ -32,6 +33,11 @@ npm-debug.log*
# Build Dir
/out

# Playwright test artifacts
/test-results
/tests/screenshots

# python
venv
__pycache__
playwright-report/
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ANCHOR_TARGET } from "@databiosphere/findable-ui/lib/components/Links/c
import { Link } from "@databiosphere/findable-ui/lib/components/Links/components/Link/link";
import { Brands, FooterText, LargeBrand, SmallBrand } from "./branding.styles";
import { TYPOGRAPHY_PROPS } from "@databiosphere/findable-ui/lib/styles/common/mui/typography";
import { VersionDisplay } from "./components/VersionDisplay/versionDisplay";

export const Branding = (): JSX.Element => {
return (
Expand Down Expand Up @@ -54,6 +55,7 @@ export const Branding = (): JSX.Element => {
url="https://www.niaid.nih.gov/research/bioinformatics-resource-centers"
/>
</FooterText>
<VersionDisplay />
</Brands>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { TYPOGRAPHY_PROPS } from "@databiosphere/findable-ui/lib/styles/common/mui/typography";

const CLIENT_VERSION = process.env.NEXT_PUBLIC_VERSION || "0.15.0";
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "";

export const VersionDisplay = (): JSX.Element => {
const [backendVersion, setBackendVersion] = useState<string | null>(null);

useEffect(() => {
if (!BACKEND_URL) {
// No backend URL configured, skip fetching
return;
}

fetch(`${BACKEND_URL}/api/v1/version`)
.then((res) => res.json())
.then((data) => setBackendVersion(data.version))
.catch(() => setBackendVersion(null)); // Gracefully handle backend unavailable
}, []);

return (
<Typography
color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400}
>
Client build: {CLIENT_VERSION}
{backendVersion && ` • Server revision: ${backendVersion}`}
</Typography>
);
};
35 changes: 35 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.pytest_cache
.mypy_cache

# Virtual environments
.env
.venv
env/
venv/

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Git
.git
.gitignore

# Documentation
*.md
docs/

# Tests
tests/
14 changes: 14 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Redis Configuration
REDIS_URL=redis://redis:6379/0

# Database Configuration (for future use)
DATABASE_URL=postgresql://user:pass@localhost/dbname

# Application Configuration
CORS_ORIGINS=http://localhost:3000,http://localhost
LOG_LEVEL=INFO
ENVIRONMENT=development

# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=60
50 changes: 50 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Multi-stage build for uv-based Python application
FROM python:3.12-slim as builder

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies into .venv
RUN uv sync --frozen --no-install-project

# Production stage
FROM python:3.12-slim as runtime

# Accept version as build argument
ARG APP_VERSION=0.15.0
ENV APP_VERSION=${APP_VERSION}

# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*

# Copy uv for runtime
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Create non-root user
RUN useradd --create-home --shell /bin/bash app

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv

# Add venv to PATH
ENV PATH="/app/.venv/bin:$PATH"

# Copy application code
COPY . .

# Change ownership
RUN chown -R app:app /app
USER app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
103 changes: 103 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# BRC Analytics Backend

FastAPI backend infrastructure for BRC Analytics.

## Features

- FastAPI REST API
- Redis caching with TTL support
- Health check endpoints
- Docker deployment with nginx reverse proxy
- uv for dependency management

## Quick Start

### Development (Local)

```bash
cd backend
uv sync
uv run uvicorn app.main:app --reload
```

API documentation: http://localhost:8000/api/docs

### Production (Docker)

```bash
# Create environment file
cp backend/.env.example backend/.env
# Edit backend/.env if needed (defaults work for local development)

# Build with version from package.json
./scripts/docker-build.sh

# Start all services (nginx + backend + redis)
docker compose up -d

# Check service health
curl http://localhost/api/v1/health

# View logs
docker compose logs -f backend

# Rebuild after code changes
docker compose up -d --build

# Stop all services
docker compose down
```

Services:

- nginx: http://localhost (reverse proxy)
- backend API: http://localhost:8000 (direct access)
- API docs: http://localhost/api/docs
- redis: localhost:6379

## API Endpoints

### Health & Monitoring

- `GET /api/v1/health` - Overall service health status
- `GET /api/v1/cache/health` - Redis cache connectivity check
- `GET /api/v1/version` - API version and environment information

### Documentation

- `GET /api/docs` - Interactive Swagger UI
- `GET /api/redoc` - ReDoc API documentation

## Configuration

Environment variables (see `.env.example`):

```bash
# Redis
REDIS_URL=redis://localhost:6379/0

# Application
CORS_ORIGINS=http://localhost:3000,http://localhost
LOG_LEVEL=INFO
```

## Testing

```bash
# Run e2e tests
npm run test:e2e

# Or with Playwright directly
npx playwright test tests/e2e/03-api-health.spec.ts
```

## Architecture

```
nginx (port 80)
├── /api/* → FastAPI backend (port 8000)
└── /* → Next.js static files

FastAPI backend
└── Redis cache (port 6379)
```
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# BRC Analytics Backend
1 change: 1 addition & 0 deletions backend/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# API package
1 change: 1 addition & 0 deletions backend/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# API v1 package
31 changes: 31 additions & 0 deletions backend/app/api/v1/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import APIRouter, Depends, HTTPException

from app.core.cache import CacheService
from app.core.dependencies import get_cache_service

router = APIRouter()


@router.get("/health")
async def cache_health(cache: CacheService = Depends(get_cache_service)):
"""Check if cache service is healthy"""
try:
# Try to set and get a test value
test_key = "health_check"
test_value = "ok"

await cache.set(test_key, test_value, ttl=60)
result = await cache.get(test_key)
await cache.delete(test_key)

if result == test_value:
return {"status": "healthy", "cache": "connected"}
else:
raise HTTPException(
status_code=503, detail="Cache service not responding correctly"
)

except Exception as e:
raise HTTPException(
status_code=503, detail=f"Cache service unhealthy: {str(e)}"
)
19 changes: 19 additions & 0 deletions backend/app/api/v1/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from datetime import datetime

from fastapi import APIRouter

from app.core.config import get_settings

router = APIRouter()
settings = get_settings()


@router.get("/health")
async def health_check():
"""Health check endpoint for monitoring system status"""
return {
"status": "healthy",
"version": settings.APP_VERSION,
"timestamp": datetime.utcnow().isoformat(),
"service": "BRC Analytics API",
}
41 changes: 41 additions & 0 deletions backend/app/api/v1/ncbi_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""API endpoints for NCBI cross-linking."""

from fastapi import APIRouter, Depends

from app.core.cache import CacheService, CacheTTL
from app.core.dependencies import get_cache_service
from app.services.ncbi_links_service import NCBILinksService

router = APIRouter()


@router.get("/organism-links.json")
async def get_organism_links(cache: CacheService = Depends(get_cache_service)):
"""Get organism links by taxonomy ID for NCBI cross-referencing"""
cache_key = "ncbi_links:organisms"

cached = await cache.get(cache_key)
if cached is not None:
return cached

service = NCBILinksService()
links = service.get_organism_links()
await cache.set(cache_key, links, ttl=CacheTTL.ONE_DAY)

return links


@router.get("/assembly-links.json")
async def get_assembly_links(cache: CacheService = Depends(get_cache_service)):
"""Get assembly links by accession for NCBI cross-referencing"""
cache_key = "ncbi_links:assemblies"

cached = await cache.get(cache_key)
if cached is not None:
return cached

service = NCBILinksService()
links = service.get_assembly_links()
await cache.set(cache_key, links, ttl=CacheTTL.ONE_DAY)

return links
16 changes: 16 additions & 0 deletions backend/app/api/v1/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter

from app.core.config import get_settings

router = APIRouter()
settings = get_settings()


@router.get("")
async def get_version():
"""Get API version and build information"""
return {
"version": settings.APP_VERSION,
"environment": settings.ENVIRONMENT,
"service": "BRC Analytics API",
}
1 change: 1 addition & 0 deletions backend/app/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Core package
Loading
Loading