import asyncio import os import time import tomllib from pathlib import Path from typing import Optional from fastapi import APIRouter, Request from loguru import logger from open_notebook.database.repository import repo_query from open_notebook.utils.version_utils import ( compare_versions, get_version_from_github, ) router = APIRouter() # In-memory cache for version check results _version_cache: dict = { "latest_version": None, "has_update": False, "timestamp": 0, "check_failed": False, } # Cache TTL in seconds (24 hours) VERSION_CACHE_TTL = 24 * 60 * 60 def get_version() -> str: """Read version from pyproject.toml""" try: pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" with open(pyproject_path, "rb") as f: pyproject = tomllib.load(f) return pyproject.get("project", {}).get("version", "unknown") except Exception as e: logger.warning(f"Could not read version from pyproject.toml: {e}") return "unknown" def get_latest_version_cached(current_version: str) -> tuple[Optional[str], bool]: """ Check for the latest version from GitHub with caching. Returns: tuple: (latest_version, has_update) - latest_version: str or None if check failed - has_update: bool indicating if update is available """ global _version_cache # Check if cache is still valid (within TTL) cache_age = time.time() - _version_cache["timestamp"] if _version_cache["timestamp"] > 0 and cache_age < VERSION_CACHE_TTL: logger.debug(f"Using cached version check result (age: {cache_age:.0f}s)") return _version_cache["latest_version"], _version_cache["has_update"] # Cache expired or not yet set if _version_cache["timestamp"] > 0: logger.info(f"Version cache expired (age: {cache_age:.0f}s), refreshing...") # Perform version check with strict error handling try: logger.info("Checking for latest version from GitHub...") # Fetch latest version from GitHub with 10-second timeout latest_version = get_version_from_github( "https://github.com/lfnovo/open-notebook", "main" ) logger.info(f"Latest version from GitHub: {latest_version}, Current version: {current_version}") # Compare versions has_update = compare_versions(current_version, latest_version) < 0 # Cache the result _version_cache["latest_version"] = latest_version _version_cache["has_update"] = has_update _version_cache["timestamp"] = time.time() _version_cache["check_failed"] = False logger.info(f"Version check complete. Update available: {has_update}") return latest_version, has_update except Exception as e: logger.warning(f"Version check failed: {e}") # Cache the failure to avoid repeated attempts _version_cache["latest_version"] = None _version_cache["has_update"] = False _version_cache["timestamp"] = time.time() _version_cache["check_failed"] = True return None, False async def check_database_health() -> dict: """ Check if database is reachable using a lightweight query. Returns: dict with 'status' ("online" | "offline") and optional 'error' """ try: # 2-second timeout for database health check result = await asyncio.wait_for( repo_query("RETURN 1"), timeout=2.0 ) if result: return {"status": "online"} return {"status": "offline", "error": "Empty result"} except asyncio.TimeoutError: logger.warning("Database health check timed out after 2 seconds") return {"status": "offline", "error": "Health check timeout"} except Exception as e: logger.warning(f"Database health check failed: {e}") return {"status": "offline", "error": str(e)} @router.get("/config") async def get_config(request: Request): """ Get frontend configuration. Returns version information and health status. Note: The frontend determines the API URL via its own runtime-config endpoint, so this endpoint no longer returns apiUrl. Also checks for version updates from GitHub (with caching and error handling). """ # Get current version current_version = get_version() # Check for updates (with caching and error handling) # This MUST NOT break the endpoint - wrapped in try-except as extra safety latest_version = None has_update = False try: latest_version, has_update = get_latest_version_cached(current_version) except Exception as e: # Extra safety: ensure version check never breaks the config endpoint logger.error(f"Unexpected error during version check: {e}") # Check database health db_health = await check_database_health() db_status = db_health["status"] if db_status == "offline": logger.warning(f"Database offline: {db_health.get('error', 'Unknown error')}") return { "version": current_version, "latestVersion": latest_version, "hasUpdate": has_update, "dbStatus": db_status, }