refactor(web-profiles/status): AST-neutral bracket hugging
This commit is contained in:
@@ -90,8 +90,7 @@ def _profile_to_dict(info) -> Dict[str, Any]:
|
||||
"distribution_name": attr("distribution_name", None),
|
||||
"distribution_version": attr("distribution_version", None),
|
||||
"distribution_source": attr("distribution_source", None),
|
||||
"has_alias": attr("alias_path", None) is not None,
|
||||
}
|
||||
"has_alias": attr("alias_path", None) is not None}
|
||||
|
||||
|
||||
def _profile_setup_command(name: str) -> str:
|
||||
@@ -128,7 +127,8 @@ def _disable_unselected_skills(profile_dir: Path, keep: List[str]) -> int:
|
||||
keep_set = {s.strip() for s in keep if s and s.strip()}
|
||||
with _hermes_home_scope(profile_dir):
|
||||
skills_root = profile_dir / "skills"
|
||||
installed = [md.parent.name for md in skills_root.rglob("SKILL.md")] if skills_root.is_dir() else []
|
||||
installed = ([md.parent.name for md in skills_root.rglob("SKILL.md")]
|
||||
if skills_root.is_dir() else [])
|
||||
cfg = load_config()
|
||||
disabled = get_disabled_skills(cfg)
|
||||
newly = 0
|
||||
@@ -202,8 +202,7 @@ def _tag_rows(rows: List[Dict[str, Any]], name: str, now: float) -> List[Dict[st
|
||||
s["profile"] = name
|
||||
s["is_default_profile"] = name == "default"
|
||||
s["is_active"] = (
|
||||
s.get("ended_at") is None
|
||||
and (now - s.get("last_active", s.get("started_at", 0))) < 300
|
||||
s.get("ended_at") is None and (now - s.get("last_active", s.get("started_at", 0))) < 300
|
||||
)
|
||||
s["archived"] = bool(s.get("archived"))
|
||||
s["pinned"] = bool(s.get("pinned"))
|
||||
@@ -413,8 +412,7 @@ def get_profiles_sessions(
|
||||
source: str = None,
|
||||
sources: str = None,
|
||||
exclude_sources: str = None,
|
||||
full: bool = False,
|
||||
):
|
||||
full: bool = False):
|
||||
"""Unified, read-only session list aggregated across ALL profiles.
|
||||
|
||||
Process-light: opens each profile's ``state.db`` directly from disk — it does NOT spawn
|
||||
@@ -441,8 +439,7 @@ def get_profiles_sessions(
|
||||
exclude_sources=_csv_list(exclude_sources) or None,
|
||||
min_message_count=max(0, min_messages),
|
||||
include_archived=archived == "include",
|
||||
archived_only=archived == "only",
|
||||
)
|
||||
archived_only=archived == "only")
|
||||
# Over-fetch per profile so the merged+sorted window is correct for the requested page.
|
||||
# Capped so a huge profile can't blow up the response.
|
||||
per_profile = min(max(limit + offset, limit), 500)
|
||||
@@ -456,8 +453,7 @@ def get_profiles_sessions(
|
||||
rows = db.list_sessions_rich(
|
||||
limit=per_profile, offset=0, order_by_last_active=order == "recent",
|
||||
# Same SQL-level blob skip as /api/sessions.
|
||||
compact_rows=not full, include_pinned=True, **filters,
|
||||
)
|
||||
compact_rows=not full, include_pinned=True, **filters)
|
||||
totals[name] = db.session_count(exclude_children=True, **filters)
|
||||
merged.extend(_tag_rows(rows, name, now))
|
||||
_read_profile_db(name, home, errors, _read)
|
||||
@@ -479,8 +475,7 @@ def get_profiles_sessions_sidebar(
|
||||
recents_exclude: str = None,
|
||||
cron_limit: int = 50,
|
||||
messaging_limit: int = 100,
|
||||
messaging_exclude: str = None,
|
||||
):
|
||||
messaging_exclude: str = None):
|
||||
"""Batched sidebar session slices — one profile-DB open per refresh.
|
||||
|
||||
The desktop sidebar needs three source-scoped windows per refresh: recents (local
|
||||
@@ -518,8 +513,7 @@ def get_profiles_sessions_sidebar(
|
||||
return db.list_sessions_rich(
|
||||
source=source, exclude_sources=exclude or None, limit=cap, offset=0,
|
||||
min_message_count=1, include_archived=False, archived_only=False,
|
||||
order_by_last_active=True, compact_rows=True, include_pinned=True,
|
||||
)
|
||||
order_by_last_active=True, compact_rows=True, include_pinned=True)
|
||||
|
||||
def _build_slices(db, cache_key):
|
||||
slices = {
|
||||
@@ -528,8 +522,7 @@ def get_profiles_sessions_sidebar(
|
||||
# and a total that shrank when you scrolled would be worse than no total at all.
|
||||
"usage": db.usage_totals(),
|
||||
"cron": _slice(db, source="cron", cap=cron_cap),
|
||||
"messaging": _slice(db, exclude=messaging_exclude_list, cap=messaging_cap),
|
||||
}
|
||||
"messaging": _slice(db, exclude=messaging_exclude_list, cap=messaging_cap)}
|
||||
_sidebar_profile_cache_put(cache_key, slices)
|
||||
return slices
|
||||
|
||||
@@ -571,8 +564,7 @@ def get_profiles_sessions_sidebar(
|
||||
"profiles_truncated": recents_truncated, "profiles_usage": profile_totals},
|
||||
"cron": {"sessions": _window(cron_rows, cron_cap)},
|
||||
"messaging": {"sessions": _window(messaging_rows, messaging_cap), "total": len(messaging_rows)},
|
||||
"errors": errors,
|
||||
}
|
||||
"errors": errors}
|
||||
|
||||
|
||||
def _merge_by_id(into: Dict[str, Dict[str, Any]], entries: List[Dict[str, Any]], child_key: str) -> None:
|
||||
@@ -601,8 +593,7 @@ def _merge_profile_tree(
|
||||
merged: Dict[str, Dict[str, Any]],
|
||||
projects: List[Dict[str, Any]],
|
||||
profile: str,
|
||||
preview_limit: int,
|
||||
) -> None:
|
||||
preview_limit: int) -> None:
|
||||
"""Fold one profile's projects into the shared tree, keyed by folder.
|
||||
|
||||
The same checkout in two profiles is one group, as is ``__no_project__`` (every profile
|
||||
@@ -677,8 +668,7 @@ def get_profiles_projects_tree(preview_limit: int = 3, session_limit: int = 2000
|
||||
"projects": sorted(merged.values(), key=lambda p: p.get("lastActive") or 0, reverse=True),
|
||||
# Ownership is per profile, so no project is "the active one" here; the desktop only
|
||||
# reads active_id to bias its overview sort.
|
||||
"active_id": None, "scoped_session_ids": scoped_session_ids, "errors": errors,
|
||||
}
|
||||
"active_id": None, "scoped_session_ids": scoped_session_ids, "errors": errors}
|
||||
|
||||
|
||||
# `gh pr create` prints the PR url and nothing else, so a tool result whose whole output IS
|
||||
@@ -803,8 +793,7 @@ async def create_profile_endpoint(body: ProfileCreate):
|
||||
{"identifier": ident, "pid": _best_effort(
|
||||
"Spawning hub-skill install %s for new profile %s failed", ident, body.name,
|
||||
fn=lambda: _spawn_install(ident))}
|
||||
for ident in ((i or "").strip() for i in body.hub_skills) if ident
|
||||
]
|
||||
for ident in ((i or "").strip() for i in body.hub_skills) if ident]
|
||||
|
||||
return {"ok": True, "name": body.name, "path": str(path), "model_set": model_set,
|
||||
"mcp_written": mcp_written, "skills_disabled": skills_disabled,
|
||||
@@ -855,8 +844,7 @@ async def get_profile_setup_command(name: str):
|
||||
_LINUX_TERMINALS = (
|
||||
("x-terminal-emulator", "-e"), ("gnome-terminal", "--"), ("konsole", "-e"),
|
||||
("xfce4-terminal", None), ("mate-terminal", None), ("lxterminal", None),
|
||||
("tilix", "-e"), ("alacritty", "-e"), ("kitty", ""), ("xterm", "-e"),
|
||||
)
|
||||
("tilix", "-e"), ("alacritty", "-e"), ("kitty", ""), ("xterm", "-e"))
|
||||
|
||||
|
||||
def _linux_terminal_commands(command: str) -> list:
|
||||
@@ -864,8 +852,7 @@ def _linux_terminal_commands(command: str) -> list:
|
||||
quoted = f"sh -lc '{command}'"
|
||||
return [
|
||||
(exe, [exe, "-e", quoted] if flag is None else [exe, *([flag] if flag else []), *sh])
|
||||
for exe, flag in _LINUX_TERMINALS
|
||||
]
|
||||
for exe, flag in _LINUX_TERMINALS]
|
||||
|
||||
|
||||
@router.post("/api/profiles/{name}/open-terminal")
|
||||
@@ -1028,8 +1015,7 @@ async def describe_profile_auto_endpoint(name: str, body: ProfileDescribeAuto):
|
||||
"description": outcome.description,
|
||||
# Only a successful generation is an auto-authored description. A failed sweep
|
||||
# leaves any existing description untouched, so don't claim it's auto-generated.
|
||||
"description_auto": bool(outcome.ok),
|
||||
}
|
||||
"description_auto": bool(outcome.ok)}
|
||||
|
||||
|
||||
# ── Export / Import ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -75,8 +75,7 @@ def _count_status_active_sessions() -> int:
|
||||
return sum(
|
||||
1 for s in sessions
|
||||
if s.get("ended_at") is None
|
||||
and (now - s.get("last_active", s.get("started_at", 0))) < 300
|
||||
)
|
||||
and (now - s.get("last_active", s.get("started_at", 0))) < 300)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -85,13 +84,11 @@ async def _status_active_sessions() -> int:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
run_in_threadpool(_count_status_active_sessions),
|
||||
timeout=_STATUS_ACTIVE_SESSIONS_TIMEOUT,
|
||||
)
|
||||
timeout=_STATUS_ACTIVE_SESSIONS_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
_log.debug(
|
||||
"/api/status active session count exceeded %.2fs; returning 0",
|
||||
_STATUS_ACTIVE_SESSIONS_TIMEOUT,
|
||||
)
|
||||
_STATUS_ACTIVE_SESSIONS_TIMEOUT)
|
||||
except Exception as exc:
|
||||
_log.debug("/api/status active session count unavailable: %s", exc)
|
||||
return 0
|
||||
@@ -107,8 +104,7 @@ async def get_ssh_ownership(request: Request):
|
||||
"ok": True,
|
||||
"sshOwnerNonce": _SSH_OWNER_NONCE,
|
||||
"protocolVersion": 1,
|
||||
"runtimeIntact": _ssh_runtime_intact(),
|
||||
}
|
||||
"runtimeIntact": _ssh_runtime_intact()}
|
||||
|
||||
|
||||
@router.get("/api/health")
|
||||
@@ -117,8 +113,7 @@ async def get_health():
|
||||
return {
|
||||
"ok": True,
|
||||
"version": __version__,
|
||||
"auth_required": bool(getattr(app.state, "auth_required", False)),
|
||||
}
|
||||
"auth_required": bool(getattr(app.state, "auth_required", False))}
|
||||
|
||||
|
||||
# Profile segment mirrors hermes_cli.profiles._PROFILE_ID_RE. Platform segment mirrors the
|
||||
@@ -202,8 +197,7 @@ def _bounded_health_probe():
|
||||
_log.warning(
|
||||
"/api/status gateway health probe exceeded %.2fs; "
|
||||
"using local status",
|
||||
_GATEWAY_HEALTH_ROUTE_TIMEOUT,
|
||||
)
|
||||
_GATEWAY_HEALTH_ROUTE_TIMEOUT)
|
||||
return False, None
|
||||
except Exception:
|
||||
return False, None
|
||||
@@ -226,8 +220,7 @@ def _project_gateway_platforms(gateway_platforms: dict, configured: "set[str] |
|
||||
platforms = {
|
||||
key: _public_platform_entry(value)
|
||||
for key, value in gateway_platforms.items()
|
||||
if _status_platform_key_allowed(key, configured)
|
||||
}
|
||||
if _status_platform_key_allowed(key, configured)}
|
||||
if gateway_running:
|
||||
return platforms
|
||||
if gateway_state == "startup_failed":
|
||||
@@ -297,8 +290,7 @@ async def _resolve_gateway_status(profile_dir: Optional[Path], health_url) -> Di
|
||||
return {
|
||||
"runtime": runtime, "gateway_running": gateway_running, "gateway_pid": liveness.pid,
|
||||
"gateway_state": gateway_state, "gateway_platforms": gateway_platforms,
|
||||
"gateway_exit_reason": gateway_exit_reason, "gateway_updated_at": gateway_updated_at,
|
||||
}
|
||||
"gateway_exit_reason": gateway_exit_reason, "gateway_updated_at": gateway_updated_at}
|
||||
|
||||
|
||||
def _auth_gate_status() -> Dict[str, Any]:
|
||||
@@ -318,9 +310,7 @@ def _auth_gate_status() -> Dict[str, Any]:
|
||||
auth_flows: list[str] = []
|
||||
try:
|
||||
from hermes_cli.dashboard_auth import (
|
||||
list_providers as _list_providers,
|
||||
list_session_providers as _list_session_providers,
|
||||
)
|
||||
list_providers as _list_providers, list_session_providers as _list_session_providers)
|
||||
auth_providers = [p.name for p in _list_providers()]
|
||||
if auth_required:
|
||||
auth_flows.append("cookie")
|
||||
@@ -357,10 +347,8 @@ async def _component_health(gateway: Dict[str, Any]) -> Dict[str, Any]:
|
||||
components: Dict[str, Any] = {
|
||||
"gateway": {
|
||||
"status": "ok" if gateway_running and gateway_state in {"running", "draining"} else "degraded",
|
||||
"state": gateway_state or ("running" if gateway_running else "stopped"),
|
||||
},
|
||||
"dashboard": DASHBOARD_HEALTH.snapshot(),
|
||||
}
|
||||
"state": gateway_state or ("running" if gateway_running else "stopped")},
|
||||
"dashboard": DASHBOARD_HEALTH.snapshot()}
|
||||
try:
|
||||
from gateway.readiness import _probe_state_db
|
||||
|
||||
@@ -371,14 +359,12 @@ async def _component_health(gateway: Dict[str, Any]) -> Dict[str, Any]:
|
||||
platform_states = [
|
||||
str(value.get("state") or value.get("status") or "").lower()
|
||||
for value in gateway_platforms.values()
|
||||
if isinstance(value, dict)
|
||||
]
|
||||
if isinstance(value, dict)]
|
||||
connected = sum(1 for state in platform_states if state in _HEALTHY_PLATFORM_STATES)
|
||||
components["platforms"] = {
|
||||
"status": "ok" if connected == len(platform_states) else "degraded",
|
||||
"configured": len(gateway_platforms),
|
||||
"connected": connected,
|
||||
}
|
||||
"connected": connected}
|
||||
return components
|
||||
|
||||
|
||||
@@ -485,8 +471,7 @@ async def get_status(profile: Optional[str] = None):
|
||||
"restart_drain_timeout": restart_drain_timeout,
|
||||
"active_sessions": active_sessions,
|
||||
**auth,
|
||||
"nous_session_valid": _nous_session_validity(),
|
||||
}
|
||||
"nous_session_valid": _nous_session_validity()}
|
||||
|
||||
# Stable per-install identity. First call may touch disk, so keep it off the loop;
|
||||
# afterwards it is a process-global cache hit. Omitted (not null) when unpersistable
|
||||
@@ -519,8 +504,7 @@ async def get_status(profile: Optional[str] = None):
|
||||
"env_path": str(get_env_path()),
|
||||
"gateway_pid": gateway["gateway_pid"],
|
||||
"gateway_health_url": _GATEWAY_HEALTH_URL,
|
||||
"gateways": topology["gateways"],
|
||||
})
|
||||
"gateways": topology["gateways"]})
|
||||
|
||||
return status
|
||||
finally:
|
||||
@@ -540,15 +524,13 @@ async def get_system_stats():
|
||||
system=_platform.system(),
|
||||
release=_platform.release(),
|
||||
version=_platform.version(),
|
||||
platform_label=_platform.platform(),
|
||||
),
|
||||
platform_label=_platform.platform()),
|
||||
"arch": _platform.machine(),
|
||||
"hostname": _platform.node(),
|
||||
"python_version": _platform.python_version(),
|
||||
"python_impl": _platform.python_implementation(),
|
||||
"hermes_version": __version__,
|
||||
"cpu_count": os.cpu_count(),
|
||||
}
|
||||
"cpu_count": os.cpu_count()}
|
||||
|
||||
def _optional(fill):
|
||||
try:
|
||||
@@ -579,7 +561,8 @@ async def get_system_stats():
|
||||
import psutil # type: ignore
|
||||
|
||||
vm = psutil.virtual_memory()
|
||||
info["memory"] = {"total": vm.total, "available": vm.available, "used": vm.used, "percent": vm.percent}
|
||||
info["memory"] = {"total": vm.total, "available": vm.available, "used": vm.used,
|
||||
"percent": vm.percent}
|
||||
for fill in (_disk, _cpu, _uptime, _process):
|
||||
_optional(fill)
|
||||
info["psutil"] = True
|
||||
@@ -617,8 +600,7 @@ async def get_curator_status():
|
||||
"last_run_at": state.get("last_run_at"),
|
||||
"min_idle_hours": _safe_call(curator, "get_min_idle_hours", None),
|
||||
"stale_after_days": _safe_call(curator, "get_stale_after_days", None),
|
||||
"archive_after_days": _safe_call(curator, "get_archive_after_days", None),
|
||||
}
|
||||
"archive_after_days": _safe_call(curator, "get_archive_after_days", None)}
|
||||
|
||||
|
||||
@router.put("/api/curator/paused")
|
||||
@@ -751,8 +733,7 @@ def _get_portal_status_sync():
|
||||
"inference_url": auth.get("inference_base_url"),
|
||||
"provider": str((model_cfg or {}).get("provider") or ""),
|
||||
"subscription_url": "https://portal.nousresearch.com/manage-subscription",
|
||||
"features": features,
|
||||
}
|
||||
"features": features}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -790,10 +771,7 @@ async def run_debug_share_endpoint(body: DebugShareRequest | None = None):
|
||||
req = body or DebugShareRequest()
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
build_debug_share,
|
||||
log_lines=max(1, min(int(req.lines), 5000)),
|
||||
redact=bool(req.redact),
|
||||
)
|
||||
build_debug_share, log_lines=max(1, min(int(req.lines), 5000)), redact=bool(req.redact))
|
||||
except RuntimeError as exc:
|
||||
# Required summary-report upload failed (offline / paste service down).
|
||||
raise HTTPException(status_code=502, detail=f"Upload failed: {exc}")
|
||||
@@ -806,8 +784,7 @@ async def run_debug_share_endpoint(body: DebugShareRequest | None = None):
|
||||
"urls": result.urls,
|
||||
"failures": result.failures,
|
||||
"redacted": result.redacted,
|
||||
"auto_delete_seconds": result.auto_delete_seconds,
|
||||
}
|
||||
"auto_delete_seconds": result.auto_delete_seconds}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -821,8 +798,7 @@ async def get_logs(
|
||||
lines: int = 100,
|
||||
level: Optional[str] = None,
|
||||
component: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
):
|
||||
search: Optional[str] = None):
|
||||
from hermes_cli.logs import _read_tail, LOG_FILES
|
||||
|
||||
log_name = LOG_FILES.get(file)
|
||||
@@ -847,8 +823,7 @@ async def get_logs(
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unknown component: {component}. "
|
||||
f"Available: {', '.join(sorted(COMPONENT_PREFIXES))}",
|
||||
)
|
||||
f"Available: {', '.join(sorted(COMPONENT_PREFIXES))}")
|
||||
else:
|
||||
comp_prefixes = None
|
||||
|
||||
@@ -857,8 +832,7 @@ async def get_logs(
|
||||
log_path, min(lines, 500) if not search else 2000,
|
||||
has_filters=has_filters,
|
||||
min_level=min_level,
|
||||
component_prefixes=comp_prefixes,
|
||||
)
|
||||
component_prefixes=comp_prefixes)
|
||||
# _read_tail doesn't support free-text search, so post-filter (case-insensitive
|
||||
# substring) here and trim to the requested line count afterward.
|
||||
if search:
|
||||
|
||||
@@ -122,8 +122,7 @@ def _fallback_profile_entry(profiles_mod, name: str, home: Path, *, is_default:
|
||||
"gateway_running": _safe(gateway_running, False),
|
||||
"description": meta("description", ""), "description_auto": meta("description_auto", False),
|
||||
"distribution_name": None, "distribution_version": None, "distribution_source": None,
|
||||
"has_alias": False,
|
||||
}
|
||||
"has_alias": False}
|
||||
|
||||
|
||||
def _fallback_profile_dicts(profiles_mod) -> List[Dict[str, Any]]:
|
||||
@@ -281,9 +280,7 @@ _TERMINAL_BACKENDS: List[Dict[str, str]] = [
|
||||
"Run commands in a Singularity/Apptainer container (HPC-friendly, rootless)."),
|
||||
("modal", "Modal", "Run commands in a Modal cloud sandbox."),
|
||||
("daytona", "Daytona", "Run commands in a Daytona cloud sandbox."),
|
||||
("ssh", "SSH", "Run commands on a remote host over SSH."),
|
||||
)
|
||||
]
|
||||
("ssh", "SSH", "Run commands on a remote host over SSH."))]
|
||||
|
||||
|
||||
def _plugin_terminal_backend_rows() -> List[Dict[str, str]]:
|
||||
@@ -349,8 +346,7 @@ def _aux_usage_rows(db, cutoff: float) -> List[Dict[str, Any]]:
|
||||
|
||||
|
||||
def _merge_aux_into_by_model(
|
||||
by_model: List[Dict[str, Any]], aux_rows: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
by_model: List[Dict[str, Any]], aux_rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Fold aux usage rows into the sessions-derived per-model list.
|
||||
|
||||
Aux usage lives only in session_model_usage (never in the sessions counters), so
|
||||
@@ -430,7 +426,6 @@ def _installed_hub_identifiers(profile: Optional[str] = None) -> dict:
|
||||
return {
|
||||
entry["identifier"]: {"name": entry.get("name"), "trust_level": entry.get("trust_level"),
|
||||
"scan_verdict": entry.get("scan_verdict")}
|
||||
for entry in lock.list_installed() if entry.get("identifier")
|
||||
}
|
||||
for entry in lock.list_installed() if entry.get("identifier")}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user