From 25d80f57231ca497a5f56b768950bbe9667160fb Mon Sep 17 00:00:00 2001
From: Madava
Date: Thu, 27 Nov 2025 11:43:35 +0100
Subject: [PATCH] Refine health checks and add polling animation
---
.gitignore | 204 +++++++++++-----------
app/README.md | 50 ++++++
app/backend/api/v1/router.py | 2 +-
app/backend/core/config.py | 24 ++-
app/backend/features/health/__init__.py | 1 -
app/backend/features/health/router.py | 23 ---
app/backend/features/health/schemas.py | 9 -
app/backend/main.py | 36 ++--
app/backend/operations/__init__.py | 1 +
app/backend/operations/health/__init__.py | 1 +
app/backend/operations/health/router.py | 74 ++++++++
app/backend/operations/health/schemas.py | 23 +++
app/backend/operations/health/service.py | 89 ++++++++++
app/backend/tests/test_health.py | 22 +--
app/backend/uv.lock | 12 +-
app/docker-compose.yml | 4 +-
app/frontend/app/globals.css | 62 +++++++
app/frontend/app/page.tsx | 81 +++++----
18 files changed, 499 insertions(+), 219 deletions(-)
create mode 100644 app/README.md
delete mode 100644 app/backend/features/health/__init__.py
delete mode 100644 app/backend/features/health/router.py
delete mode 100644 app/backend/features/health/schemas.py
create mode 100644 app/backend/operations/__init__.py
create mode 100644 app/backend/operations/health/__init__.py
create mode 100644 app/backend/operations/health/router.py
create mode 100644 app/backend/operations/health/schemas.py
create mode 100644 app/backend/operations/health/service.py
diff --git a/.gitignore b/.gitignore
index a826518..5f5a745 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,54 +5,54 @@
# ----------------------------------------------------------------------------
# Python: Byte-compiled & Cache
# ----------------------------------------------------------------------------
-**/__pycache__/
-**/*.py[cod]
-**/*$py.class
-**/*.so
-**/.cython_debug/
-**/cython_debug/
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.cython_debug/
+cython_debug/
# ----------------------------------------------------------------------------
# Python: Packaging & Distribution
# ----------------------------------------------------------------------------
-**/build/
-**/dist/
-**/develop-eggs/
-**/downloads/
-**/eggs/
-**/.eggs/
-**/lib/
-**/lib64/
-**/parts/
-**/sdist/
-**/wheels/
-**/share/python-wheels/
-**/*.egg-info/
-**/.installed.cfg
-**/*.egg
-**/MANIFEST
-**/*.manifest
-**/*.spec
+build/
+dist/
+develop-eggs/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+*.manifest
+*.spec
# Installer logs
-**/pip-log.txt
-**/pip-delete-this-directory.txt
+pip-log.txt
+pip-delete-this-directory.txt
# ----------------------------------------------------------------------------
# Python: Virtual Environments & Dependency Managers
# ----------------------------------------------------------------------------
# Virtual environments
-**/.venv
-**/venv/
-**/env/
-**/ENV/
-**/env.bak/
-**/venv.bak/
-**/.pixi/
-**/__pypackages__/
+.venv
+venv/
+env/
+ENV/
+env.bak/
+venv.bak/
+.pixi/
+__pypackages__/
# pyenv
-**/.python-version
+.python-version
# Dependency lock files (usually committed, uncomment if you prefer to ignore)
# Pipfile.lock
@@ -62,48 +62,48 @@
# pixi.lock
# Tool-specific
-**/.tox/
-**/.nox/
-**/.pdm-python
-**/.pdm-build/
-**/.poetry.toml
-**/.pdm.toml
+.tox/
+.nox/
+.pdm-python
+.pdm-build/
+.poetry.toml
+.pdm.toml
# ----------------------------------------------------------------------------
# Python: Testing & Coverage
# ----------------------------------------------------------------------------
-**/htmlcov/
-**/.coverage
-**/.coverage.*
-**/.cache
-**/nosetests.xml
-**/coverage.xml
-**/*.cover
-**/*.py.cover
-**/.hypothesis/
-**/.pytest_cache/
-**/cover/
-**/.ruff_cache/
+htmlcov/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+.ruff_cache/
# ----------------------------------------------------------------------------
# Python: Development & IDE
# ----------------------------------------------------------------------------
# Jupyter / IPython
-**/.ipynb_checkpoints
-**/profile_default/
-**/ipython_config.py
+.ipynb_checkpoints
+profile_default/
+ipython_config.py
# Type checkers & linters
-**/.mypy_cache/
-**/.dmypy.json
-**/dmypy.json
-**/.pyre/
-**/.pytype/
+.mypy_cache/
+.dmypy.json
+dmypy.json
+.pyre/
+.pytype/
# Project / IDE settings
-**/.spyderproject
-**/.spyproject
-**/.ropeproject
+.spyderproject
+.spyproject
+.ropeproject
# PyCharm / JetBrains (uncomment to ignore entire folder)
# .idea/
@@ -115,78 +115,78 @@
# Python: Frameworks & Tools
# ----------------------------------------------------------------------------
# Django
-**/*.log
-**/local_settings.py
-**/db.sqlite3
-**/db.sqlite3-journal
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
# Flask
-**/instance/
-**/.webassets-cache
+instance/
+.webassets-cache
# Scrapy
-**/.scrapy
+.scrapy
# Celery
-**/celerybeat-schedule
-**/celerybeat-schedule.*
-**/celerybeat.pid
+celerybeat-schedule
+celerybeat-schedule.*
+celerybeat.pid
# Sphinx / MkDocs / Marimo
-**/docs/_build/
-**//site
-**/marimo/_static/
-**/marimo/_lsp/
-**/__marimo__/
+docs/_build/
+/site
+marimo/_static/
+marimo/_lsp/
+__marimo__/
# Streamlit secrets
-**/.streamlit/secrets.toml
+.streamlit/secrets.toml
# ----------------------------------------------------------------------------
# Next.js / Node.js
# ----------------------------------------------------------------------------
# Dependencies
-**/node_modules
-**/.pnp
-**/.pnp.js
-**/.pnp.loader.mjs
+node_modules
+.pnp
+.pnp.js
+.pnp.loader.mjs
# Build outputs
-**/.next/
-**/out/
-**/build/
+.next/
+out/
+build/
# TypeScript
-**/*.tsbuildinfo
-**/next-env.d.ts
+*.tsbuildinfo
+next-env.d.ts
# Testing (Jest, etc.)
-**/coverage
+coverage
# Vercel
-**/.vercel
+.vercel
# ----------------------------------------------------------------------------
# General / OS / Security
# ----------------------------------------------------------------------------
# Environment variables
-**/.env
-**/.env*.local
-**/.envrc
+.env
+.env*.local
+.envrc
# OS generated files
-**/.DS_Store
-**/Thumbs.db
-**/*.pem
+.DS_Store
+Thumbs.db
+*.pem
# Logs & debug
-**/npm-debug.log*
-**/yarn-debug.log*
-**/yarn-error.log*
-**/*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+*.log
# PyPI config
-**/.pypirc
+.pypirc
# ==============================================================================
# End of file
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..fc8f3e0
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,50 @@
+# 1. Run Applicaiton
+
+1. Removes all cached Python packages stored by pip, removes local Python cache files, clears the cache used by uv, and forcibly clear the cache for Node.js.
+
+ ```bash
+ uv tool install cleanpy
+ pip cache purge && cleanpy . && uv cache clean && npm cache clean --force
+ ```
+
+2. Resolve dependencies from your *pyproject.toml* and upgrade all packages. Synchronize the virtual environment with the dependencies specified in the *uv.lock* including packages needed for development.
+
+ ```bash
+ cd backend
+ uv lock --upgrade
+ uv sync --dev
+ ```
+
+3. Lint and check code for errors, style issues, and potential bugs, and try to fix them. Discover and run tests in *tests/*.
+
+ ```bash
+ uv run ruff check --fix && uv run pytest
+ ```
+
+4. Starts a local development API server, visible at port 8000, and automatically reloads the server as you make code changes.
+
+ ```bash
+ uv run uvicorn src.main:app --reload --port 8000
+ ```
+
+5. Scans dependencies for security vulnerabilities and attempts to automatically fix them by force-updating to the latest secure versions.
+
+ ```bash
+ cd ..
+ cd frontend
+ npm audit fix --force
+ ```
+
+6. Install dependencies from *package.json*, then update those dependencies to the latest allowed versions based on version ranges. Next, check the source code for stylistic and syntax errors according to configured rules. Finally, compile or bundle the application for deployment or production use.
+
+ ```bash
+ cd frontend
+ npm install && npm update && npm run lint && npm run build
+ ```
+
+7. Execute start script in *package.json*, launch your Node.js application in production mode.
+
+ ```bash
+ cd frontend
+ npm run start
+ ```
diff --git a/app/backend/api/v1/router.py b/app/backend/api/v1/router.py
index 0b9c164..9c700d5 100644
--- a/app/backend/api/v1/router.py
+++ b/app/backend/api/v1/router.py
@@ -1,4 +1,4 @@
-"""Version 1 API router aggregator."""
+"""Version 1 API router aggregator for features."""
from fastapi import APIRouter
diff --git a/app/backend/core/config.py b/app/backend/core/config.py
index 25d30b5..8b6969e 100644
--- a/app/backend/core/config.py
+++ b/app/backend/core/config.py
@@ -1,18 +1,30 @@
"""Environment configuration derived from environment variables."""
from functools import lru_cache
-
from pydantic_settings import BaseSettings, SettingsConfigDict
-
class Settings(BaseSettings):
"""Application settings."""
- service_name: str = "backend"
+ service_name: str = "avaaz-backend" # Used as a functional ID
environment: str = "development"
- database_url: str = (
- "postgresql+psycopg://postgres:postgres@postgres:5432/avaaz"
- )
+
+ title: str = "Avaaz Language Tutoring API"
+ description: str ="""
+ # Avaaz Language Tutoring API
+
+ This API powers the **avaaz.ai** mobile and web applications, providing the robust backend services for our AI-driven oral language skills tutor. The platform is specifically engineered to help students achieve oral proficiency using adaptive, conversational AI agents.
+
+ ## Key Services Provided:
+ * **Conversational AI Engine:** Facilitates ultra-low-latency speech-to-speech interaction and provides instant corrective feedback (grammar, pronunciation, fluency).
+ * **Curriculum Management:** Delivers structured, CEFR aligned lessons and scenarios focused on real-life immigrant contexts (healthcare, workplace, school).
+ * **Assessment & Gamification:** Manages progress tracking, mock oral exam simulations, performance summaries, and motivational mechanics (streaks, badges).
+ * **Cross-Platform Sync:** Ensures seamless learning continuity and progress synchronization across all user devices.
+ """
+
+ version: str = "0.1.0"
+
+ database_url: str = "postgresql+psycopg://postgres:postgres@postgres:5432/avaaz"
model_config = SettingsConfigDict(env_prefix="", case_sensitive=False)
diff --git a/app/backend/features/health/__init__.py b/app/backend/features/health/__init__.py
deleted file mode 100644
index ffd3d8f..0000000
--- a/app/backend/features/health/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Health check feature."""
diff --git a/app/backend/features/health/router.py b/app/backend/features/health/router.py
deleted file mode 100644
index 3f339ee..0000000
--- a/app/backend/features/health/router.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Health endpoints."""
-
-from fastapi import APIRouter, Depends
-
-from core.config import Settings, get_settings
-from features.health.schemas import HealthResponse
-
-router = APIRouter(prefix="/health", tags=["health"])
-
-
-def build_health_payload(settings: Settings) -> HealthResponse:
- """Build health response payload."""
- return HealthResponse(
- status="ok",
- service=settings.service_name,
- environment=settings.environment,
- )
-
-
-@router.get("/", response_model=HealthResponse)
-def health(settings: Settings = Depends(get_settings)) -> HealthResponse:
- """Return lightweight health status."""
- return build_health_payload(settings)
diff --git a/app/backend/features/health/schemas.py b/app/backend/features/health/schemas.py
deleted file mode 100644
index 2c8b191..0000000
--- a/app/backend/features/health/schemas.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""Pydantic schemas for health responses."""
-
-from pydantic import BaseModel, Field
-
-
-class HealthResponse(BaseModel):
- status: str = Field(default="ok", description="Overall service status.")
- service: str = Field(default="backend", description="Service name.")
- environment: str = Field(default="development", description="Runtime environment.")
diff --git a/app/backend/main.py b/app/backend/main.py
index de369c4..e6245e1 100644
--- a/app/backend/main.py
+++ b/app/backend/main.py
@@ -1,21 +1,26 @@
"""Application entrypoint and router wiring."""
-from fastapi import Depends, FastAPI
+from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
-from api.v1.router import router as api_v1_router
-from core.config import Settings, get_settings
-from features.health.router import build_health_payload
-from features.health.schemas import HealthResponse
+from api.v1.router import router as api_v1_router
+from operations.health.router import router as health_router
+
+from core.config import get_settings
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
+
+ settings = get_settings()
+
app = FastAPI(
- title="avaaz-backend",
- version="0.1.0",
+ title=settings.title,
+ description=settings.description,
+ version=settings.version,
docs_url="/docs",
- redoc_url="/redoc",
+ redoc_url=None,
+ openapi_url="/openapi.json",
)
# CORS for local dev (frontend on 3000). Tighten/override in prod.
@@ -27,22 +32,9 @@ def create_app() -> FastAPI:
allow_headers=["*"],
)
+ app.include_router(health_router)
app.include_router(api_v1_router, prefix="/api/v1")
- @app.get("/healthz", tags=["health"], response_model=HealthResponse)
- def liveness(settings: Settings = Depends(get_settings)) -> HealthResponse:
- """Liveness probe: process is up."""
- return build_health_payload(settings)
-
- @app.get("/readyz", tags=["health"], response_model=HealthResponse)
- def readiness(settings: Settings = Depends(get_settings)) -> HealthResponse:
- """
- Readiness probe: instance can serve traffic.
-
- Extend to check dependencies (DB, cache, queues) when they are added.
- """
- return build_health_payload(settings)
-
return app
diff --git a/app/backend/operations/__init__.py b/app/backend/operations/__init__.py
new file mode 100644
index 0000000..7b20f0a
--- /dev/null
+++ b/app/backend/operations/__init__.py
@@ -0,0 +1 @@
+"""Domain operation modules."""
diff --git a/app/backend/operations/health/__init__.py b/app/backend/operations/health/__init__.py
new file mode 100644
index 0000000..ad8226d
--- /dev/null
+++ b/app/backend/operations/health/__init__.py
@@ -0,0 +1 @@
+"""Health feature package."""
diff --git a/app/backend/operations/health/router.py b/app/backend/operations/health/router.py
new file mode 100644
index 0000000..ad2431f
--- /dev/null
+++ b/app/backend/operations/health/router.py
@@ -0,0 +1,74 @@
+"""Health endpoints."""
+
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import PlainTextResponse
+
+from core.config import Settings, get_settings
+from operations.health.schemas import HealthStatus
+from operations.health.service import get_detailed_health, readiness_check
+
+router = APIRouter(prefix="/health", tags=["health"])
+
+
+@router.get(
+ "/live",
+ summary="Liveness Probe",
+ response_class=PlainTextResponse,
+ status_code=200,
+)
+def liveness() -> str:
+ """
+ **Liveness Probe:** Confirms the application process is running and responsive.
+
+ This endpoint is used by automated systems (like Kubernetes) to determine if
+ the instance should be kept running or restarted. It must be extremely lightweight,
+ performing no deep checks on external dependencies.
+
+ **Success Response:** HTTP 200 OK with "live" body.
+ **Failure Response:** Endpoint timeout (no response).
+ """
+ return "live"
+
+
+@router.get(
+ "/ready",
+ summary="Readiness Probe",
+ response_class=PlainTextResponse,
+ status_code=200,
+)
+async def readiness() -> str:
+ """
+ **Readiness Probe:** Determines if the application can accept user traffic.
+
+ This endpoint is used by load balancers to route traffic. It performs deep checks
+ on all critical dependencies (e.g., database, message queue).
+
+ **Success Response:** HTTP 200 OK with "ready" body.
+ **Failure Response:** HTTP 503 Service Unavailable if any critical dependency fails.
+ """
+ ok = await readiness_check()
+ if not ok:
+ raise HTTPException(status_code=503, detail="not ready")
+
+ return "ready"
+
+
+@router.get(
+ "",
+ summary="Detailed Health Status Page",
+ response_model=HealthStatus,
+ status_code=200,
+)
+async def detailed_health(settings: Settings = Depends(get_settings)) -> HealthStatus:
+ """
+ **Detailed Status Page:** Provides granular health information for human operators.
+
+ This endpoint runs all readiness checks and returns a structured JSON object.
+ The top-level HTTP status code reflects the overall application health (200 OK or 503 Service Unavailable).
+ """
+ detailed_health = await get_detailed_health(settings)
+
+ if detailed_health.status != "pass":
+ raise HTTPException(status_code=503, detail="not ready")
+
+ return detailed_health
diff --git a/app/backend/operations/health/schemas.py b/app/backend/operations/health/schemas.py
new file mode 100644
index 0000000..dd39d3d
--- /dev/null
+++ b/app/backend/operations/health/schemas.py
@@ -0,0 +1,23 @@
+"""Pydantic schemas for health responses."""
+
+from datetime import datetime
+
+from pydantic import BaseModel
+
+class ComponentCheck(BaseModel):
+ name: str
+ # pass | warn | fail
+ status: str
+ time: datetime | None = None
+ output: str | None = None
+ observedValue: float | int | None = None
+ observedUnit: str | None = None
+
+class HealthStatus(BaseModel):
+ # pass | warn | fail
+ status: str
+ version: str | None = None
+ environment: str | None = None
+ serviceName: str | None = None
+ description: str | None = None
+ checks: dict[str, ComponentCheck]
diff --git a/app/backend/operations/health/service.py b/app/backend/operations/health/service.py
new file mode 100644
index 0000000..886bd43
--- /dev/null
+++ b/app/backend/operations/health/service.py
@@ -0,0 +1,89 @@
+import asyncio
+from datetime import datetime, timezone
+
+from core.config import Settings
+from operations.health.schemas import ComponentCheck, HealthStatus
+
+
+async def check_database_status() -> ComponentCheck:
+ """Checking the primary database connection with a timeout."""
+ try:
+ # Simulate an async DB call (replace with actual logic)
+ await asyncio.wait_for(asyncio.sleep(0.042), timeout=0.1)
+
+ return ComponentCheck(name = "postgres",
+ status = "pass",
+ time = datetime.now(timezone.utc),
+ observedValue = 42,
+ observedUnit = "ms")
+ except asyncio.TimeoutError:
+ return ComponentCheck(name = "postgres",
+ status = "fail",
+ time = datetime.now(timezone.utc),
+ output = "Check timed out")
+ except Exception as e:
+ return ComponentCheck(name = "postgres",
+ status = "fail",
+ time = datetime.now(timezone.utc),
+ output = str(e))
+
+async def check_media_server_status() -> ComponentCheck:
+ """Checking the media server connection with a timeout."""
+ try:
+ # Simulate a successful async queue call
+ await asyncio.wait_for(asyncio.sleep(0.02), timeout=0.05)
+
+ return ComponentCheck(name = "livekit",
+ status = "pass",
+ time = datetime.now(timezone.utc),
+ observedValue = 20,
+ observedUnit = "ms")
+ except asyncio.TimeoutError:
+ return ComponentCheck(name = "livekit",
+ status = "fail",
+ time = datetime.now(timezone.utc),
+ output = "Check timed out")
+ except Exception as e:
+ return ComponentCheck(name = "livekit",
+ status = "fail",
+ time = datetime.now(timezone.utc),
+ output = str(e))
+
+async def readiness_check() -> bool:
+ """
+ *Extend readiness_check() as dependencies are added; it must return True/False.*
+ """
+
+ # Run all critical checks
+ db = await check_database_status()
+ media_server = await check_media_server_status()
+
+ # Check if all statuses are 'pass'
+ if db.status == 'pass' and media_server.status == 'pass':
+ return True
+ else:
+ return False
+
+async def get_detailed_health(settings: Settings) -> HealthStatus:
+ """
+ Build detailed health payload aligned with common status formats.
+
+ status: pass | warn | fail
+ """
+
+ # Run all critical checks
+ db = await check_database_status()
+ media_server = await check_media_server_status()
+
+ # Check if all statuses are 'pass'
+ overall = "pass"
+ if db.status != 'pass' or media_server.status != 'pass':
+ overall = "fail"
+
+ return HealthStatus(status = overall,
+ version = settings.version,
+ environment = settings.environment,
+ serviceName = settings.service_name,
+ description = settings.title,
+ checks = {"Database": db,
+ "Media Server": media_server})
diff --git a/app/backend/tests/test_health.py b/app/backend/tests/test_health.py
index 55f3e9e..7adc081 100644
--- a/app/backend/tests/test_health.py
+++ b/app/backend/tests/test_health.py
@@ -6,25 +6,27 @@ from main import app
client = TestClient(app)
-
def test_liveness_ok():
- response = client.get("/healthz")
+ response = client.get("/health/live")
assert response.status_code == 200
- data = response.json()
- assert data["status"] == "ok"
- assert data["service"] == "backend"
- assert data["environment"]
+ assert response.text == "live"
def test_readiness_ok():
- response = client.get("/readyz")
+ response = client.get("/health/ready")
assert response.status_code == 200
- assert response.json()["status"] == "ok"
+ assert response.text == "ready"
+def test_detailed_health_pass():
+ resp = client.get("/health")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["status"] == "pass"
+ assert isinstance(body["checks"], dict)
@given(st.text(min_size=0, max_size=16))
@settings(max_examples=10)
def test_health_resilient_to_query_noise(noise: str):
- resp = client.get("/healthz", params={"noise": noise})
+ resp = client.get("/health/live", params={"noise": noise})
assert resp.status_code == 200
- assert resp.json()["status"] == "ok"
+ assert resp.text == "live"
diff --git a/app/backend/uv.lock b/app/backend/uv.lock
index 9c9c16d..a3652e4 100644
--- a/app/backend/uv.lock
+++ b/app/backend/uv.lock
@@ -272,14 +272,14 @@ wheels = [
[[package]]
name = "hypothesis"
-version = "6.148.2"
+version = "6.148.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sortedcontainers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4a/99/a3c6eb3fdd6bfa01433d674b0f12cd9102aa99630689427422d920aea9c6/hypothesis-6.148.2.tar.gz", hash = "sha256:07e65d34d687ddff3e92a3ac6b43966c193356896813aec79f0a611c5018f4b1", size = 469984, upload-time = "2025-11-18T20:21:17.047Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/3d/41da3727e5f3e6b0c79b9657946c742e2f61d24edcde3e1660e337509586/hypothesis-6.148.3.tar.gz", hash = "sha256:bd81221740d8658473060ad900dc831f889f156fdb41210ba2f47cfad10a66ed", size = 469896, upload-time = "2025-11-27T06:34:09.419Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/d2/c2673aca0127e204965e0e9b3b7a0e91e9b12993859ac8758abd22669b89/hypothesis-6.148.2-py3-none-any.whl", hash = "sha256:bf8ddc829009da73b321994b902b1964bcc3e5c3f0ed9a1c1e6a1631ab97c5fa", size = 536986, upload-time = "2025-11-18T20:21:15.212Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/61/8c9fd9397eb46ac54d974be8b9e619c386d6b47a462d8df962ebb79980f9/hypothesis-6.148.3-py3-none-any.whl", hash = "sha256:e7dd193da9800234ec5e1541c1eddde4bddff49b53faf690ba68a0af55a7abb3", size = 536925, upload-time = "2025-11-27T06:34:06.978Z" },
]
[[package]]
@@ -320,7 +320,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.12.4"
+version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -328,9 +328,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
diff --git a/app/docker-compose.yml b/app/docker-compose.yml
index cf7fd05..10fa197 100644
--- a/app/docker-compose.yml
+++ b/app/docker-compose.yml
@@ -116,7 +116,7 @@ services:
test:
[
"CMD-SHELL",
- "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz').close()\"",
+ "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/live').close()\"",
]
interval: 30s
timeout: 5s
@@ -147,7 +147,7 @@ services:
test:
[
"CMD-SHELL",
- "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz').close()\"",
+ "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/live').close()\"",
]
interval: 15s
timeout: 5s
diff --git a/app/frontend/app/globals.css b/app/frontend/app/globals.css
index 367b0d4..142a74a 100644
--- a/app/frontend/app/globals.css
+++ b/app/frontend/app/globals.css
@@ -27,6 +27,38 @@ body {
padding: 40px 24px 80px;
}
+.starfall {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ opacity: 0;
+ background-image: radial-gradient(2px 2px at 10% 20%, #d6e9ff, transparent),
+ radial-gradient(2px 2px at 30% 0%, #c7ddff, transparent),
+ radial-gradient(2px 2px at 60% 10%, #c0f2ff, transparent),
+ radial-gradient(2px 2px at 80% 25%, #f3d5ff, transparent),
+ radial-gradient(2px 2px at 50% 5%, #bfe2ff, transparent),
+ radial-gradient(2px 2px at 15% 5%, #d6e9ff, transparent);
+ background-size: 200px 200px, 260px 260px, 220px 220px, 240px 240px,
+ 210px 210px, 230px 230px;
+ z-index: 3;
+}
+
+.starfall.active {
+ opacity: 1;
+ animation: starfall-move 1.4s ease-out forwards;
+}
+
+@keyframes starfall-move {
+ 0% {
+ transform: translateY(-30px);
+ opacity: 0.9;
+ }
+ 100% {
+ transform: translateY(80px);
+ opacity: 0;
+ }
+}
+
.nebula {
position: absolute;
inset: 0;
@@ -138,6 +170,36 @@ body {
color: #cdd5f5;
}
+.pulse {
+ position: relative;
+ overflow: hidden;
+}
+
+.pulse::after {
+ content: "";
+ position: absolute;
+ inset: -2px;
+ border-radius: 999px;
+ background: radial-gradient(
+ circle at 20% 20%,
+ rgba(124, 211, 255, 0.35),
+ transparent 50%
+ );
+ animation: ping 0.9s ease-out;
+ pointer-events: none;
+}
+
+@keyframes ping {
+ 0% {
+ opacity: 0.75;
+ transform: scale(0.98);
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(1.25);
+ }
+}
+
.status-pill {
display: inline-flex;
align-items: center;
diff --git a/app/frontend/app/page.tsx b/app/frontend/app/page.tsx
index 05da5e1..71f0570 100644
--- a/app/frontend/app/page.tsx
+++ b/app/frontend/app/page.tsx
@@ -3,15 +3,17 @@
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
-type HealthResponse = {
+type HealthSummary = {
status: string;
- service: string;
- environment: string;
+ version?: string;
+ serviceId?: string;
+ description?: string;
+ checks: Record;
};
export default function Home() {
- const [health, setHealth] = useState(null);
- const [ready, setReady] = useState(null);
+ const [health, setHealth] = useState(null);
+ const [ready, setReady] = useState(null);
const [error, setError] = useState(null);
const [readyError, setReadyError] = useState(null);
const [attemptedHealthUrl, setAttemptedHealthUrl] = useState(
@@ -21,6 +23,8 @@ export default function Home() {
null
);
const [lastUpdated, setLastUpdated] = useState(null);
+ const [isPolling, setIsPolling] = useState(false);
+ const [starsActive, setStarsActive] = useState(false);
const apiBase = useMemo(
() => process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000",
@@ -30,38 +34,48 @@ export default function Home() {
useEffect(() => {
async function fetchHealth() {
try {
- const url = `${apiBase}/healthz`;
+ const url = `${apiBase}/health`;
setAttemptedHealthUrl(url);
const res = await fetch(url);
if (!res.ok) throw new Error(`Health check failed: ${res.status}`);
- const payload = (await res.json()) as HealthResponse;
+ const payload = (await res.json()) as HealthSummary;
setHealth(payload);
setError(null);
+ return true;
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
setError(message);
setHealth(null);
+ return false;
}
}
async function fetchReady() {
try {
- const url = `${apiBase}/readyz`;
+ const url = `${apiBase}/health/ready`;
setAttemptedReadyUrl(url);
const res = await fetch(url);
if (!res.ok) throw new Error(`Ready check failed: ${res.status}`);
- const payload = (await res.json()) as HealthResponse;
+ const payload = await res.text();
setReady(payload);
setReadyError(null);
+ return true;
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
setReadyError(message);
setReady(null);
+ return false;
}
}
const run = async () => {
- await Promise.allSettled([fetchHealth(), fetchReady()]);
+ setIsPolling(true);
+ const [healthOk, readyOk] = await Promise.all([fetchHealth(), fetchReady()]);
setLastUpdated(new Date().toLocaleTimeString());
+ if (healthOk && readyOk) {
+ setStarsActive(true);
+ setTimeout(() => setStarsActive(false), 1400);
+ }
+ setTimeout(() => setIsPolling(false), 900);
};
run();
@@ -75,12 +89,13 @@ export default function Home() {
: "status-pill bad";
const readyClass =
- ready && ready.status === "ok" && !readyError
+ ready && !readyError
? "status-pill ok"
: "status-pill bad";
return (
+
@@ -109,10 +126,10 @@ export default function Home() {
- Health: {health && !error ? "ok" : "unhealthy"}
+ Health: {health && !error ? health.status : "unhealthy"}
- Ready: {ready && !readyError ? "ok" : "not ready"}
+ Ready: {ready && !readyError ? ready : "not ready"}
{lastUpdated && (
Updated {lastUpdated}
@@ -124,28 +141,26 @@ export default function Home() {
- /healthz
+ /health
- {health && !error ? "ok" : "unhealthy"}
+ {health && !error ? health.status : "unhealthy"}
- {attemptedHealthUrl || `${apiBase}/healthz`}
+ {attemptedHealthUrl || `${apiBase}/health`}
{health ? (
-
-
Service
-
{health.service}
-
-
-
Environment
-
{health.environment}
-
+ {Object.entries(health.checks || {}).map(([key, val]) => (
+
+ ))}
) : (
{error || "Waiting for response..."}
@@ -154,27 +169,19 @@ export default function Home() {
- /readyz
+ /health/ready
- {ready && !readyError ? "ok" : "not ready"}
+ {ready && !readyError ? ready : "not ready"}
- {attemptedReadyUrl || `${apiBase}/readyz`}
+ {attemptedReadyUrl || `${apiBase}/health/ready`}
{ready ? (
-
-
Service
-
{ready.service}
-
-
-
Environment
-
{ready.environment}
-
Status
-
{ready.status}
+
{ready}
) : (