Release 1.0.0 with server info console

This commit is contained in:
Artem Kokos
2026-05-21 20:46:04 +07:00
parent 85c840ba1b
commit 61b21c63ea
12 changed files with 766 additions and 5 deletions

22
app/api/routes/system.py Normal file
View File

@@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends, Request
from app.api.deps import AuthContext, verify_token
from app.api.schemas import ServerInfoResponse
from app.core.server_info import build_server_info
router = APIRouter(dependencies=[Depends(verify_token)])
@router.get(
"/info",
response_model=ServerInfoResponse,
response_model_exclude_none=True,
)
async def get_system_info(
request: Request,
auth: AuthContext = Depends(verify_token),
):
return build_server_info(
observed_base_url=str(request.base_url),
include_diagnostics=auth.is_admin,
)

View File

@@ -178,3 +178,35 @@ class RescanResponse(BaseModel):
removed_offline: int
pending_removal: int
online: int
class ServerBuildInfoResponse(BaseModel):
version: str | None = None
git_sha: str | None = None
build_date: str | None = None
class ServerUrlInfoResponse(BaseModel):
observed_base_url: str | None = None
configured_public_base_url: str | None = None
effective_public_base_url: str | None = None
class ServerConfigurationStatusResponse(BaseModel):
configured: bool
master_key_configured: bool
scan_network_configured: bool
public_base_url_configured: bool
build_metadata_complete: bool
class ServerInfoResponse(BaseModel):
app_name: str
instance_name: str | None = None
timezone: str | None = None
uptime_seconds: int
diagnostics_visible: bool
started_at: str | None = None
build: ServerBuildInfoResponse | None = None
urls: ServerUrlInfoResponse | None = None
configuration: ServerConfigurationStatusResponse | None = None

186
app/core/server_info.py Normal file
View File

@@ -0,0 +1,186 @@
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