fix(dashboard): keep profiles list resilient

This commit is contained in:
vincez-hms-coder 2026-04-27 08:40:11 -04:00
parent 4523965de9
commit 58c07867e3
4 changed files with 102 additions and 9 deletions

View file

@ -2118,18 +2118,65 @@ class ProfileSoulUpdate(BaseModel):
content: str
def _profile_attr(info, name: str, default: Any = None) -> Any:
try:
return getattr(info, name)
except Exception:
return default
def _profile_to_dict(info) -> Dict[str, Any]:
return {
"name": info.name,
"path": str(info.path),
"is_default": info.is_default,
"model": info.model,
"provider": info.provider,
"has_env": info.has_env,
"skill_count": info.skill_count,
"name": _profile_attr(info, "name", ""),
"path": str(_profile_attr(info, "path", "")),
"is_default": bool(_profile_attr(info, "is_default", False)),
"model": _profile_attr(info, "model"),
"provider": _profile_attr(info, "provider"),
"has_env": bool(_profile_attr(info, "has_env", False)),
"skill_count": int(_profile_attr(info, "skill_count", 0) or 0),
}
def _fallback_profile_dicts(profiles_mod) -> List[Dict[str, Any]]:
def _safe(callable_, default):
try:
return callable_()
except Exception:
return default
profiles: List[Dict[str, Any]] = []
default_home = profiles_mod._get_default_hermes_home()
if default_home.is_dir():
model, provider = _safe(lambda: profiles_mod._read_config_model(default_home), (None, None))
profiles.append({
"name": "default",
"path": str(default_home),
"is_default": True,
"model": model,
"provider": provider,
"has_env": (default_home / ".env").exists(),
"skill_count": _safe(lambda: profiles_mod._count_skills(default_home), 0),
})
profiles_root = profiles_mod._get_profiles_root()
if profiles_root.is_dir():
for entry in sorted(profiles_root.iterdir()):
if not entry.is_dir() or not profiles_mod._PROFILE_ID_RE.match(entry.name):
continue
model, provider = _safe(lambda entry=entry: profiles_mod._read_config_model(entry), (None, None))
profiles.append({
"name": entry.name,
"path": str(entry),
"is_default": False,
"model": model,
"provider": provider,
"has_env": (entry / ".env").exists(),
"skill_count": _safe(lambda entry=entry: profiles_mod._count_skills(entry), 0),
})
return profiles
def _resolve_profile_dir(name: str) -> Path:
"""Validate ``name`` and resolve to its directory or raise an HTTPException."""
from hermes_cli import profiles as profiles_mod
@ -2145,7 +2192,11 @@ def _resolve_profile_dir(name: str) -> Path:
@app.get("/api/profiles")
async def list_profiles_endpoint():
from hermes_cli import profiles as profiles_mod
return {"profiles": [_profile_to_dict(p) for p in profiles_mod.list_profiles()]}
try:
return {"profiles": [_profile_to_dict(p) for p in profiles_mod.list_profiles()]}
except Exception:
_log.exception("GET /api/profiles failed; falling back to profile directory scan")
return {"profiles": _fallback_profile_dicts(profiles_mod)}
@app.post("/api/profiles")

View file

@ -0,0 +1,11 @@
"""Static dashboard tests for the Profiles navigation copy."""
from pathlib import Path
def test_profiles_nav_label_uses_short_multi_agents_copy():
en_i18n = Path(__file__).resolve().parents[2] / "web" / "src" / "i18n" / "en.ts"
content = en_i18n.read_text(encoding="utf-8")
assert 'profiles: "profiles : multi agents"' in content
assert "Profiles: Running Multiple Agents" not in content

View file

@ -596,6 +596,37 @@ class TestNewEndpoints:
names = [p["name"] for p in resp.json()["profiles"]]
assert "default" in names
def test_profiles_list_falls_back_when_profile_listing_fails(self, monkeypatch):
from hermes_constants import get_hermes_home
import hermes_cli.profiles as profiles_mod
hermes_home = get_hermes_home()
hermes_home.mkdir(parents=True, exist_ok=True)
(hermes_home / "config.yaml").write_text(
"model:\n provider: openrouter\n name: anthropic/claude-sonnet-4.6\n",
encoding="utf-8",
)
named = hermes_home / "profiles" / "multi-agent"
named.mkdir(parents=True)
(named / ".env").write_text("EXAMPLE=1\n", encoding="utf-8")
(named / "skills" / "demo").mkdir(parents=True)
(named / "skills" / "demo" / "SKILL.md").write_text("---\nname: demo\n---\n", encoding="utf-8")
monkeypatch.setattr(
profiles_mod,
"list_profiles",
lambda: (_ for _ in ()).throw(RuntimeError("boom")),
)
resp = self.client.get("/api/profiles")
assert resp.status_code == 200
profiles = {p["name"]: p for p in resp.json()["profiles"]}
assert profiles["default"]["is_default"] is True
assert profiles["default"]["provider"] == "openrouter"
assert profiles["multi-agent"]["has_env"] is True
assert profiles["multi-agent"]["skill_count"] == 1
def test_profiles_create_rename_delete_round_trip(self, monkeypatch):
# Stub gateway service teardown so the test doesn't shell out to
# launchctl/systemctl on the host.

View file

@ -74,7 +74,7 @@ export const en: Translations = {
documentation: "Documentation",
keys: "Keys",
logs: "Logs",
profiles: "Profiles: Running Multiple Agents",
profiles: "profiles : multi agents",
sessions: "Sessions",
skills: "Skills",
},