fix: tunnel button stuck on "Connecting..." due to settings validation

Root cause: toggleTunnelFromWelcome() sent the entire settings object
back to PUT /api/settings, but the Zod schema uses .strict() which
rejects unknown fields (lastUsedCase, localEchoEnabled, etc.). The PUT
silently failed, so the tunnel never started.

Fix: send only {tunnelEnabled: true/false} instead of the full blob.
Also added polling fallback for tunnel status and server-side re-broadcast
when tunnel is already running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-27 16:45:05 +01:00
co-authored by Claude Opus 4.6
parent 1b0a540919
commit 93ebe3f07e
4 changed files with 39 additions and 6 deletions
+6
View File
@@ -1,5 +1,11 @@
# codeman
## 0.2.4
### Patch Changes
- Fix tunnel button not working: settings PUT was rejected by strict Zod validation when sending full settings blob; now sends only `{tunnelEnabled}`. Added polling fallback for tunnel status in case SSE events are missed.
## 0.2.3
### Patch Changes
+1 -1
View File
@@ -50,7 +50,7 @@ When user says "COM":
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
**Version**: 0.2.3 (must match `package.json`)
**Version**: 0.2.4 (must match `package.json`)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codeman",
"version": "0.2.3",
"version": "0.2.4",
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+31 -4
View File
@@ -8946,17 +8946,16 @@ class CodemanApp {
const isActive = btn.classList.contains('active');
btn.disabled = true;
try {
const res = await fetch('/api/settings');
const current = res.ok ? await res.json() : {};
const newEnabled = !isActive;
current.tunnelEnabled = newEnabled;
await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(current),
body: JSON.stringify({ tunnelEnabled: newEnabled }),
});
if (newEnabled) {
this._showTunnelConnecting();
// Poll tunnel status as fallback in case SSE event is missed
this._pollTunnelStatus();
} else {
this._dismissTunnelConnecting();
this.showToast('Tunnel stopped', 'info');
@@ -9001,6 +9000,8 @@ class CodemanApp {
}
_dismissTunnelConnecting() {
clearTimeout(this._tunnelPollTimer);
this._tunnelPollTimer = null;
const toast = document.getElementById('tunnelConnectingToast');
if (toast) {
toast.classList.remove('show');
@@ -9010,6 +9011,32 @@ class CodemanApp {
if (btn) btn.classList.remove('connecting');
}
_pollTunnelStatus(attempt = 0) {
if (attempt > 15) return; // give up after ~30s
this._tunnelPollTimer = setTimeout(async () => {
try {
const res = await fetch('/api/tunnel/status');
const status = await res.json();
if (status.running && status.url) {
// Tunnel is up — update UI
this._dismissTunnelConnecting();
this._updateTunnelUrlDisplay(status.url);
const welcomeVisible = document.getElementById('welcomeOverlay')?.classList.contains('visible');
if (welcomeVisible) {
this._updateWelcomeTunnelBtn(true, status.url, true);
this.showToast('Tunnel active', 'success');
} else {
this._updateWelcomeTunnelBtn(true, status.url);
this.showToast(`Tunnel active: ${status.url}`, 'success');
this.showTunnelQR();
}
return;
}
} catch { /* ignore */ }
this._pollTunnelStatus(attempt + 1);
}, 2000);
}
_updateWelcomeTunnelBtn(active, url, firstAppear = false) {
const btn = document.getElementById('welcomeTunnelBtn');
if (btn) {