From 73a38617e37d72475353289db3d750195d0dad10 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 24 Aug 2026 23:07:28 +1000 Subject: [PATCH] fix: prevent Ollama config refresh resets --- README.md | 2 +- dashboard/dist/index.js | 6 +++++- dashboard/manifest.json | 2 +- dashboard/plugin_api.py | 18 +++++++++++++----- plugin.yaml | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7882c24..43784de 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The dashboard now includes a connection panel in the header. Enter an Ollama bas The plugin probes the configured local endpoint and known Docker endpoints, and tests a configured remote endpoint. A successful connection reports the Ollama version and installed model count. The active local endpoint continues to drive chat, status, load, and runtime telemetry. A URL saved from the dashboard takes precedence over the initial `OLLAMA_HOST` environment fallback, so the field can correct or replace an inherited container setting. -When both distinct local and remote endpoints are online, downloading or re-downloading a model opens a destination chooser. The selected target is recorded on the job and the pull is sent to that endpoint. Remote downloads do not alter the local installed-model inventory. The connection list marks saved rows and provides a **Remove saved** action with confirmation. Removing the saved Local endpoint clears the active override and restores `OLLAMA_HOST` or automatic discovery. Removing Remote only clears the saved remote URL; detected-but-unsaved rows cannot be removed. If Local and Remote contain the same URL, the dashboard marks the duplicate and treats it as one download target; enter the actual second Ollama server URL to enable independent remote downloads. +When both distinct local and remote endpoints are online, downloading or re-downloading a model opens a destination chooser. The selected target is recorded on the job and the pull is sent to that endpoint. Remote downloads do not alter the local installed-model inventory. The plugin writes this configuration atomically and fixes the dashboard status-poll race that could let a slower older `/status` response overwrite a newer saved state. The config root is captured once per dashboard process from `HERMES_HOME`/Hermes home, rather than changing between requests. Removing the saved Local endpoint clears the active override and restores `OLLAMA_HOST` or automatic discovery. Removing Remote only clears the saved remote URL; detected-but-unsaved rows cannot be removed. If Local and Remote contain the same URL, the dashboard marks the duplicate and treats it as one download target; enter the actual second Ollama server URL to enable independent remote downloads. The plugin supports both deployment types: diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index 8c19a26..bdae2ab 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -376,7 +376,11 @@ var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1]; var targetDialogState = React.useState(null), targetDialog = targetDialogState[0], setTargetDialog = targetDialogState[1]; var loadingState = React.useState(true), loading = loadingState[0], setLoading = loadingState[1]; - function load() { return fetchJSON(API + "/status").then(function (value) { setData(value); setLoading(false); return value; }).catch(function (err) { setNotice({ error: err.message || String(err) }); setLoading(false); }); } + var loadSequence = React.useRef(0); + function load() { + var sequence = ++loadSequence.current; + return fetchJSON(API + "/status").then(function (value) { if (sequence !== loadSequence.current) return value; setData(value); setLoading(false); return value; }).catch(function (err) { if (sequence === loadSequence.current) { setNotice({ error: err.message || String(err) }); setLoading(false); } }); + } React.useEffect(function () { load(); var timer = setInterval(load, 5000); return function () { clearInterval(timer); }; }, []); function action(kind, name, selectedTarget) { if (kind === "delete" && !window.confirm("Remove " + name + " from Ollama?")) return; diff --git a/dashboard/manifest.json b/dashboard/manifest.json index baa6de6..f512e77 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "label": "Ollama Models", "description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.", "icon": "Cpu", - "version": "1.5.9", + "version": "1.5.10", "tab": {"path": "/ollama-manager", "position": "after:models"}, "entry": "dist/index.js", "css": "dist/style.css", diff --git a/dashboard/plugin_api.py b/dashboard/plugin_api.py index bc96ff1..14d8964 100644 --- a/dashboard/plugin_api.py +++ b/dashboard/plugin_api.py @@ -262,8 +262,11 @@ FAMILY_STRENGTHS = { } +_PLUGIN_HOME_ROOT = Path(os.environ.get("HERMES_HOME", "").strip()).expanduser().resolve() if os.environ.get("HERMES_HOME", "").strip() else Path(get_hermes_home()).resolve() + + def _home() -> Path: - path = get_hermes_home() / "ollama-manager" + path = _PLUGIN_HOME_ROOT / "ollama-manager" path.mkdir(parents=True, exist_ok=True) return path @@ -296,11 +299,16 @@ def _read_connections() -> dict[str, str]: def _write_connections(value: dict[str, str]) -> None: path = _home() / CONNECTIONS_FILE - path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp") + temporary.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") try: - path.chmod(0o600) - except OSError: - pass + temporary.chmod(0o600) + os.replace(temporary, path) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass def _apply_saved_connection() -> None: diff --git a/plugin.yaml b/plugin.yaml index 4751048..30da538 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: ollama-manager -version: 1.5.9 +version: 1.5.10 description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry. auto_install_dependencies: true python_dependencies: