Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9121,11 +9121,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")
Expand Down
10 changes: 10 additions & 0 deletions tests/hermes_cli/test_web_server_profile_unification.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ def test_config_raw_round_trip_scoped(self, client, isolated_profiles):
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
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion web/src/pages/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {});
}, []);

Expand Down
Loading