diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index c4b028a602e..ad70a46491f 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -9374,11 +9374,18 @@ class RawConfigUpdate(BaseModel): @app.get("/api/config/raw") async def get_config_raw(profile: Optional[str] = None): + """Raw config.yaml text plus its resolved path. + + ``path`` is resolved inside ``_profile_scope`` so the Config page header + shows the file the switched profile actually reads/writes — /api/status's + ``config_path`` is machine-global and always reports the dashboard + process's own profile, which is wrong under the global profile switcher. + """ with _profile_scope(profile): path = get_config_path() if not path.exists(): - return {"yaml": ""} - return {"yaml": path.read_text(encoding="utf-8")} + return {"yaml": "", "path": str(path)} + return {"yaml": path.read_text(encoding="utf-8"), "path": str(path)} @app.put("/api/config/raw") diff --git a/tests/hermes_cli/test_web_server_profile_unification.py b/tests/hermes_cli/test_web_server_profile_unification.py index d458348f128..fed4d189260 100644 --- a/tests/hermes_cli/test_web_server_profile_unification.py +++ b/tests/hermes_cli/test_web_server_profile_unification.py @@ -92,6 +92,16 @@ class TestProfileScopedConfig: resp = client.get("/api/config/raw") assert "Io/Volcano" not in resp.json()["yaml"] + def test_config_raw_path_reflects_requested_profile(self, client, isolated_profiles): + """The Config page header shows /api/config/raw's ``path`` — it must + point at the SWITCHED profile's config.yaml, not the dashboard's own + (the stale-path bug reported after the profile unification launch).""" + resp = client.get("/api/config/raw", params={"profile": "worker_beta"}) + assert resp.status_code == 200 + assert resp.json()["path"] == str(isolated_profiles["worker_beta"] / "config.yaml") + resp = client.get("/api/config/raw") + assert resp.json()["path"] == str(isolated_profiles["default"] / "config.yaml") + def test_unknown_profile_404(self, client, isolated_profiles): resp = client.get("/api/config", params={"profile": "ghost"}) assert resp.status_code == 404 diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 5d7784f8c51..7349e8596d4 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -432,7 +432,7 @@ export const api = { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config }), }), - getConfigRaw: () => fetchJSON<{ yaml: string }>("/api/config/raw"), + getConfigRaw: () => fetchJSON<{ yaml: string; path?: string }>("/api/config/raw"), saveConfigRaw: (yaml_text: string) => fetchJSON<{ ok: boolean }>("/api/config/raw", { method: "PUT", diff --git a/web/src/pages/ConfigPage.tsx b/web/src/pages/ConfigPage.tsx index 50ad3261a3f..c6bff27f1d7 100644 --- a/web/src/pages/ConfigPage.tsx +++ b/web/src/pages/ConfigPage.tsx @@ -177,9 +177,19 @@ export default function ConfigPage() { .getDefaults() .then(setDefaults) .catch(() => {}); + // getConfigRaw is profile-scoped (fetchJSON appends ?profile=), so its + // `path` reflects the switched profile's config.yaml. /api/status's + // config_path is machine-global (the dashboard's own profile) — wrong + // header under the global profile switcher, so it's only a fallback. + api + .getConfigRaw() + .then((resp) => { + if (resp.path) setConfigPath(resp.path); + }) + .catch(() => {}); api .getStatus() - .then((resp) => setConfigPath(resp.config_path)) + .then((resp) => setConfigPath((prev) => prev ?? resp.config_path)) .catch(() => {}); }, []);