"""Process-wide overlay state — what was fetched, what applied, what failed.
One object, written under a lock by the fetch/apply path and read by /health,
the swarm announce payload, the notify endpoint and the admin panel. It is a
plain summary: the document itself lives in ``merged`` only for as long as the
process runs, and in the cache file between runs.
"""
from __future__ import annotations
import threading
from dataclasses import asdict, dataclass, field
# One refresh at a time. A hive notification and an announce hint can arrive
# together; stacking two applies would let an older document overwrite a newer
# one, and the second validate against a half-updated base.
lock = threading.Lock()
@dataclass
class OverlayState:
configured: bool = False # HIVE_URL + swarm key present
enabled: bool = True # HIVE_CONFIG_ENABLED and not NAVI_OVERLAY=off
source: str = "local" # local | hive | cache
version: int = 0 # document version assigned by the hive
hash: str = "" # effective hash for this instance
layers: list[str] = field(default_factory=list)
status: str = "disabled" # disabled|applied|staged|rejected|unreachable|not_targeted
errors: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
unknown_fields: list[str] = field(default_factory=list)
forbidden_fields: list[str] = field(default_factory=list)
restart_required: list[str] = field(default_factory=list)
hot_applied: list[str] = field(default_factory=list)
last_fetch: str | None = None
last_error: str | None = None
boot_guard: bool = False
# The validated, cleaned overlay currently in force. Sections:
# settings / profiles / llm / mcp_servers. Each is the merge-patch to
# apply over the *local* base, never over a previous overlay.
merged: dict = field(default_factory=dict, repr=False)
# Base settings and profiles as loaded from disk, captured once, so a
# refresh can rebuild from the local config instead of stacking.
base_settings: object | None = field(default=None, repr=False)
_state = OverlayState()
def get_state() -> OverlayState:
return _state
def reset_state() -> None:
"""Tests only: forget everything, including the captured base."""
global _state
_state = OverlayState()
def status_dict() -> dict:
data = asdict(_state)
data.pop("merged", None)
data.pop("base_settings", None)
return data
def status_summary() -> dict:
"""Compact form for the swarm announce payload (stored in hive.meta)."""
return {
"version": _state.version,
"hash": _state.hash,
"status": _state.status,
"restart_required": list(_state.restart_required),
"hot_applied": list(_state.hot_applied),
# Validation errors first (a rejected document), then the reason a
# fetch failed — an "unreachable" machine that does not say why is
# useless in /health and in the hive's status view.
"error": (_state.errors[0] if _state.errors else None) or _state.last_error,
}