from __future__ import annotations from dataclasses import asdict, dataclass from datetime import datetime, timezone import os from pathlib import Path import socket from app.api.deps import get_master_key APP_NAME = "Ignis Core" PROJECT_ROOT = Path(__file__).resolve().parents[2] VERSION_FILE = PROJECT_ROOT / "VERSION" SERVER_STARTED_AT = datetime.now(timezone.utc) @dataclass(frozen=True) class ServerBuildInfo: version: str git_sha: str | None build_date: str | None @dataclass(frozen=True) class ServerUrlInfo: observed_base_url: str | None configured_public_base_url: str | None effective_public_base_url: str | None @dataclass(frozen=True) class ServerConfigurationStatus: configured: bool master_key_configured: bool scan_network_configured: bool public_base_url_configured: bool build_metadata_complete: bool def _clean_env(name: str) -> str | None: value = os.getenv(name, "").strip() return value or None def _normalize_url(value: str | None) -> str | None: if not value: return None normalized = value.strip().rstrip("/") return normalized or None def _read_version_file() -> str | None: try: value = VERSION_FILE.read_text(encoding="utf-8").strip() except OSError: return None return value or None def get_app_version() -> str: return ( _clean_env("IGNIS_BUILD_VERSION") or _read_version_file() or "1.0.0" ) def _resolve_git_ref(git_dir: Path, ref_name: str) -> str | None: ref_path = git_dir / ref_name if ref_path.exists(): try: value = ref_path.read_text(encoding="utf-8").strip() except OSError: return None return value or None packed_refs = git_dir / "packed-refs" if not packed_refs.exists(): return None try: lines = packed_refs.read_text(encoding="utf-8").splitlines() except OSError: return None for line in lines: if not line or line.startswith("#") or line.startswith("^"): continue sha, _, candidate_ref = line.partition(" ") if candidate_ref.strip() == ref_name: return sha.strip() or None return None def _read_git_sha() -> str | None: git_dir = PROJECT_ROOT / ".git" if not git_dir.exists(): return None head_path = git_dir / "HEAD" try: head_value = head_path.read_text(encoding="utf-8").strip() except OSError: return None if not head_value: return None if head_value.startswith("ref: "): resolved = _resolve_git_ref(git_dir, head_value[5:].strip()) if not resolved: return None return resolved[:12] return head_value[:12] def get_build_info() -> ServerBuildInfo: git_sha = _clean_env("IGNIS_GIT_SHA") or _read_git_sha() build_date = _clean_env("IGNIS_BUILD_DATE") return ServerBuildInfo( version=get_app_version(), git_sha=git_sha, build_date=build_date, ) def get_instance_name() -> str: return _clean_env("IGNIS_INSTANCE_NAME") or socket.gethostname() def get_server_urls(observed_base_url: str | None) -> ServerUrlInfo: configured_public_base_url = _normalize_url(_clean_env("IGNIS_PUBLIC_BASE_URL")) observed = _normalize_url(observed_base_url) return ServerUrlInfo( observed_base_url=observed, configured_public_base_url=configured_public_base_url, effective_public_base_url=configured_public_base_url or observed, ) def get_configuration_status(build_info: ServerBuildInfo) -> ServerConfigurationStatus: master_key_configured = get_master_key() is not None public_base_url_configured = _clean_env("IGNIS_PUBLIC_BASE_URL") is not None scan_network_configured = _clean_env("SCAN_NETWORK") is not None build_metadata_complete = bool(build_info.version and build_info.git_sha and build_info.build_date) return ServerConfigurationStatus( configured=master_key_configured, master_key_configured=master_key_configured, scan_network_configured=scan_network_configured, public_base_url_configured=public_base_url_configured, build_metadata_complete=build_metadata_complete, ) def get_uptime_seconds() -> int: delta = datetime.now(timezone.utc) - SERVER_STARTED_AT return max(int(delta.total_seconds()), 0) def build_server_info( *, observed_base_url: str | None = None, include_diagnostics: bool, ) -> dict: payload = { "app_name": APP_NAME, "uptime_seconds": get_uptime_seconds(), "diagnostics_visible": include_diagnostics, } if not include_diagnostics: return payload build_info = get_build_info() payload["instance_name"] = get_instance_name() payload["timezone"] = os.getenv("APP_TIMEZONE", "Asia/Novosibirsk") payload["started_at"] = SERVER_STARTED_AT.isoformat() payload["build"] = { "version": build_info.version, "git_sha": build_info.git_sha, "build_date": build_info.build_date, } payload["urls"] = asdict(get_server_urls(observed_base_url)) payload["configuration"] = asdict(get_configuration_status(build_info)) return payload