From 7352c7a0ab011dfdd4738ebe82f655f7cdf19d11 Mon Sep 17 00:00:00 2001 From: Joakim Ohlander Date: Tue, 24 Feb 2026 19:31:43 +0100 Subject: [PATCH 1/2] Add consistent Restart button to watcher overview row The Service and Zoekt rows in the dashboard overview always show a Restart button when running, but the Watcher row had no action button when active. This adds a Restart button matching the same pattern and consolidates the version-mismatch restart to use the same function. Co-Authored-By: Claude Opus 4.6 --- public/index.html | 91 +++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/public/index.html b/public/index.html index 7c9515e..71f9a2d 100644 --- a/public/index.html +++ b/public/index.html @@ -420,7 +420,7 @@

Watcher Status

watcherDot.className = 'overview-dot dot-green'; watcherDetail.textContent = `Active${hash} \u00b7 ${files} files, ${assets} assets ingested`; } - watcherAction.innerHTML = ''; + watcherAction.innerHTML = ''; } else { watcherDot.className = 'overview-dot dot-red'; watcherDetail.textContent = 'No watcher connected \u2014 file changes won\'t be indexed'; @@ -442,7 +442,7 @@

Watcher Status

`Service (${serviceGitHash}) \u00b7 Watcher (${watcherGitHash})`; const actionsEl = document.getElementById('version-alert-actions'); actionsEl.innerHTML = ` - + `; } else { versionAlert.classList.remove('visible'); @@ -476,54 +476,6 @@

Watcher Status

} } - async function stopAndRestartWatcher(btn) { - const status = document.getElementById('version-alert-status'); - btn.disabled = true; - btn.textContent = 'Stopping watcher...'; - status.textContent = ''; - try { - await fetch('/api/watcher/stop', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workspace: WorkspaceContext.active }) - }); - status.textContent = 'Waiting for watcher to stop...'; - const stopped = await waitForWatcherState(false, { maxAttempts: 20, intervalMs: 1000 }); - if (!stopped) { - status.textContent = 'Watcher did not stop in time. Try closing it manually.'; - btn.textContent = 'Restart Watcher'; - btn.disabled = false; - return; - } - btn.textContent = 'Starting watcher...'; - status.textContent = ''; - const startResp = await fetch('/api/watcher/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workspace: WorkspaceContext.active }) - }); - if (!startResp.ok) { - const err = await startResp.json(); - status.textContent = err.error || 'Failed to start watcher'; - btn.textContent = 'Restart Watcher'; - btn.disabled = false; - return; - } - status.textContent = 'Waiting for new watcher...'; - const started = await waitForWatcherState(true); - if (started) { - loadAllWithOverview(); - } else { - status.textContent = 'Watcher started but still reconciling. Refresh shortly.'; - btn.textContent = 'Restart Watcher'; - btn.disabled = false; - } - } catch (err) { - status.textContent = 'Error: ' + err.message; - btn.textContent = 'Restart Watcher'; - btn.disabled = false; - } - } async function overviewStartWatcher(btn) { btn.disabled = true; @@ -555,6 +507,45 @@

Watcher Status

} } + async function overviewRestartWatcher(btn) { + btn.disabled = true; + btn.textContent = 'Restarting...'; + try { + await fetch('/api/watcher/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workspace: WorkspaceContext.active }) + }); + const stopped = await waitForWatcherState(false, { maxAttempts: 20, intervalMs: 1000 }); + if (!stopped) { + btn.textContent = 'Stop timed out'; + setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + return; + } + const startResp = await fetch('/api/watcher/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workspace: WorkspaceContext.active }) + }); + if (!startResp.ok) { + const data = await startResp.json(); + btn.textContent = data.error || 'Failed'; + setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + return; + } + const started = await waitForWatcherState(true); + if (started) { + loadAllWithOverview(); + } else { + btn.textContent = 'Timed out'; + setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + } + } catch (err) { + btn.textContent = 'Error'; + setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + } + } + async function overviewRestartZoekt(btn) { btn.disabled = true; btn.textContent = 'Restarting...'; From e3805c3d84bfe2433ebbed44d258283ce38ff00e Mon Sep 17 00:00:00 2001 From: Joakim Ohlander Date: Tue, 24 Feb 2026 19:35:24 +0100 Subject: [PATCH 2/2] Fix orphaned status span, button label reset, and unchecked stop response - Remove unused #version-alert-status span and its CSS rule since overviewRestartWatcher uses button text for progress feedback - Capture original button text so error/timeout resets restore the correct label (e.g. "Restart Watcher" vs "Restart") - Check /api/watcher/stop response status and surface errors instead of silently falling through to the timeout path Co-Authored-By: Claude Opus 4.6 --- public/index.html | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/public/index.html b/public/index.html index 71f9a2d..442acc4 100644 --- a/public/index.html +++ b/public/index.html @@ -37,7 +37,7 @@ .btn-primary { padding: 6px 16px; background: #0e639c; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; } .btn-primary:hover { background: #1177bb; } .btn-primary:disabled { background: #333; color: #666; cursor: default; } - .version-alert-status { font-size: 12px; color: #808080; } + .overview-bar { display: flex; gap: 16px; margin-bottom: 20px; } .overview-item { flex: 1; background: #252526; border: 1px solid #3e3e3e; border-radius: 6px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; } .overview-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } @@ -442,8 +442,7 @@

Watcher Status

`Service (${serviceGitHash}) \u00b7 Watcher (${watcherGitHash})`; const actionsEl = document.getElementById('version-alert-actions'); actionsEl.innerHTML = ` - - `; + `; } else { versionAlert.classList.remove('visible'); } @@ -508,18 +507,25 @@

Watcher Status

} async function overviewRestartWatcher(btn) { + const originalText = btn.textContent; btn.disabled = true; btn.textContent = 'Restarting...'; try { - await fetch('/api/watcher/stop', { + const stopResp = await fetch('/api/watcher/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workspace: WorkspaceContext.active }) }); + if (!stopResp.ok) { + const data = await stopResp.json().catch(() => ({})); + btn.textContent = data.error || 'Stop failed'; + setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 5000); + return; + } const stopped = await waitForWatcherState(false, { maxAttempts: 20, intervalMs: 1000 }); if (!stopped) { btn.textContent = 'Stop timed out'; - setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 5000); return; } const startResp = await fetch('/api/watcher/start', { @@ -528,9 +534,9 @@

Watcher Status

body: JSON.stringify({ workspace: WorkspaceContext.active }) }); if (!startResp.ok) { - const data = await startResp.json(); + const data = await startResp.json().catch(() => ({})); btn.textContent = data.error || 'Failed'; - setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 5000); return; } const started = await waitForWatcherState(true); @@ -538,11 +544,11 @@

Watcher Status

loadAllWithOverview(); } else { btn.textContent = 'Timed out'; - setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 5000); } } catch (err) { btn.textContent = 'Error'; - setTimeout(() => { btn.textContent = 'Restart'; btn.disabled = false; }, 5000); + setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 5000); } }