This commit is contained in:
root
2026-05-17 05:22:53 +00:00
parent 07aa02bca6
commit 03dc47a569
24 changed files with 941 additions and 93 deletions

View File

@@ -479,6 +479,13 @@ def load_stack_env() -> dict[str, str]:
return values
def _env_flag(env: dict[str, str], key: str, default: bool = True) -> bool:
raw = str(env.get(key, "1" if default else "0")).strip().lower()
if raw in {"", "default"}:
return default
return raw not in {"0", "false", "no", "off", "disable", "disabled"}
def _probe_tcp_port(host: str, port: int, timeout: float = 2.0) -> dict[str, Any]:
t0 = time.monotonic()
try:
@@ -524,6 +531,207 @@ def _probe_http_get(url: str, timeout: float = 2.5) -> dict[str, Any]:
}
def _resolve_host_ipv4s(host: str) -> list[str]:
ips: list[str] = []
try:
for info in socket.getaddrinfo(host, None, family=socket.AF_INET):
ip = str(info[4][0]).strip()
if ip and ip not in ips:
ips.append(ip)
except OSError:
return []
return ips
def _deployment_file_check(check_id: str, label: str, path: Path, detail: str) -> dict[str, Any]:
return {
"id": check_id,
"label": label,
"ok": path.is_file(),
"kind": "file",
"target": str(path),
"detail": detail,
}
def _deployment_http_check(
check_id: str,
label: str,
url: str,
*,
expected_codes: tuple[int, ...] = (200,),
timeout: float = 2.5,
) -> dict[str, Any]:
probe = _probe_http_get(url, timeout=timeout)
code = int(probe.get("http_code") or 0)
ok = bool(probe.get("reachable")) and code in expected_codes
detail = f"HTTP {code}" if code else (probe.get("error") or "unreachable")
if probe.get("latency_ms") is not None:
detail = f"{detail} · {probe['latency_ms']} ms"
return {
"id": check_id,
"label": label,
"ok": ok,
"kind": "http",
"url": url,
"http_code": code,
"latency_ms": probe.get("latency_ms"),
"reachable": bool(probe.get("reachable")),
"detail": detail,
"error": str(probe.get("error") or ""),
}
def deployment_report() -> dict[str, Any]:
env = load_stack_env()
summary = stack_summary()
mdns_host = str(summary.get("mdns_host") or "live.local")
web_port = str(env.get("PORT", os.environ.get("PORT", "8001")) or "8001")
lan_ips = list(summary.get("ips") or [])
primary_lan_ip = lan_ips[0] if lan_ips else ""
live_edge_enabled = _env_flag(env, "ENABLE_LIVE_EDGE_PROXY", True)
mdns_enabled = _env_flag(env, "ENABLE_MDNS", True)
control_assets = BASE_DIR / "web-console" / "out" / "index.html"
checks: list[dict[str, Any]] = [
_deployment_file_check(
"stack_env",
"system-stack.env",
STACK_ENV,
"Live stack environment file",
),
_deployment_file_check(
"console_assets",
"web-console/out/index.html",
control_assets,
"Exported web control panel assets",
),
_deployment_http_check(
"api_loopback",
"FastAPI loopback",
f"http://127.0.0.1:{web_port}/health",
),
]
if primary_lan_ip:
checks.append(
_deployment_http_check(
"api_lan",
"FastAPI LAN bind",
f"http://{primary_lan_ip}:{web_port}/health",
)
)
if live_edge_enabled:
checks.append(
_deployment_http_check(
"edge_loopback",
"live-edge loopback",
"http://127.0.0.1/health",
)
)
if primary_lan_ip:
checks.append(
_deployment_http_check(
"edge_lan",
"live-edge LAN bind",
f"http://{primary_lan_ip}/health",
)
)
mdns_ips = _resolve_host_ipv4s(mdns_host) if mdns_enabled else []
checks.append(
{
"id": "mdns_resolution",
"label": "mDNS resolution",
"ok": bool(mdns_ips) if mdns_enabled else True,
"kind": "dns",
"host": mdns_host,
"ips": mdns_ips,
"detail": ", ".join(mdns_ips) if mdns_ips else ("disabled" if not mdns_enabled else "unresolved"),
}
)
if mdns_enabled and mdns_ips:
checks.append(
_deployment_http_check(
"mdns_api",
"live.local direct API",
f"http://{mdns_host}:{web_port}/health",
)
)
if live_edge_enabled:
checks.append(
_deployment_http_check(
"mdns_edge",
"live.local edge proxy",
f"http://{mdns_host}/health",
)
)
required_ids = {
"stack_env",
"console_assets",
"api_loopback",
}
if primary_lan_ip:
required_ids.add("api_lan")
if live_edge_enabled:
required_ids.add("edge_loopback")
if primary_lan_ip:
required_ids.add("edge_lan")
if mdns_enabled:
required_ids.add("mdns_resolution")
if mdns_ips:
required_ids.add("mdns_api")
if live_edge_enabled:
required_ids.add("mdns_edge")
pass_count = sum(1 for item in checks if item.get("ok"))
total_count = len(checks)
failing = [item for item in checks if item["id"] in required_ids and not item.get("ok")]
hints: list[str] = []
failed_ids = {item["id"] for item in failing}
if "api_loopback" in failed_ids:
hints.append("live-console service is unhealthy on 127.0.0.1; inspect systemd logs or scripts/launch.py.")
if "api_lan" in failed_ids:
hints.append("The control plane answers on loopback but not on the LAN IP; check HOST binding and local firewall rules.")
if "edge_loopback" in failed_ids or "edge_lan" in failed_ids:
hints.append("Port 80 reverse proxy is unhealthy; restart live-edge.service or rerun scripts/linux/reinstall_live_edge.sh.")
if "mdns_resolution" in failed_ids:
hints.append("live.local does not resolve locally; check avahi-daemon, libnss-mdns, and same-LAN mDNS visibility.")
if "mdns_api" in failed_ids and "mdns_resolution" not in failed_ids:
hints.append("live.local resolves but :PORT is not reachable through that hostname; verify mDNS points to the correct LAN IP.")
if "mdns_edge" in failed_ids and "mdns_resolution" not in failed_ids:
hints.append("live.local resolves but port 80 proxy is unhealthy; verify live-edge.service and the upstream PORT value.")
if "console_assets" in failed_ids:
hints.append("The exported web console is missing; run upgrade-live.sh or rebuild web-console before relying on live.local.")
urls = {
"loopback_api": f"http://127.0.0.1:{web_port}/health",
"mdns_control": f"http://{mdns_host}:{web_port}/",
"mdns_root": f"http://{mdns_host}/",
"lan_control": f"http://{primary_lan_ip}:{web_port}/" if primary_lan_ip else "",
"lan_root": f"http://{primary_lan_ip}/" if primary_lan_ip else "",
}
return {
"ready": not failing,
"pass_count": pass_count,
"total_count": total_count,
"required_total": len(required_ids),
"hostname": summary.get("hostname"),
"mdns_host": mdns_host,
"primary_lan_ip": primary_lan_ip,
"port": web_port,
"live_edge_enabled": live_edge_enabled,
"mdns_enabled": mdns_enabled,
"checks": checks,
"failing_checks": failing,
"hints": hints,
"urls": urls,
}
def _iface_speed_mbps(name: str) -> int | None:
path = Path("/sys/class/net") / name / "speed"
if not path.is_file():