fix(network): footer Local toggle dead in Tauri (window.confirm no-op) (#169)

The footer Local/Network pill called window.confirm() before enabling, but
window.confirm is a no-op in the Tauri webview (returns false), so the enable
action was silently swallowed — the button appeared to do nothing. Replace it
with a reliable in-app confirm popover (Cancel/Enable). The backend endpoint
was always working (verified: enable opens a real listener on the share port).
Adds a regression test for the Local -> confirm -> Enable -> POST flow.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-05-30 17:11:47 +05:30
committed by GitHub
co-authored by Claude Opus 4.8
parent c993e14072
commit 3901a3cf4a
3 changed files with 74 additions and 4 deletions
+29
View File
@@ -166,3 +166,32 @@
opacity: 0.5;
cursor: not-allowed;
}
.net-toggle__confirm-actions {
display: flex;
gap: 6px;
margin-top: 8px;
}
.net-toggle__enable {
background: rgba(184, 187, 38, 0.15);
border: 1px solid rgba(184, 187, 38, 0.4);
color: #b8bb26;
font-size: 11px;
font-weight: 600;
font-family: inherit;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
}
.net-toggle__enable:hover { background: rgba(184, 187, 38, 0.25); }
.net-toggle__cancel {
background: transparent;
border: 1px solid var(--border, #504945);
color: inherit;
font-size: 11px;
font-family: inherit;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
}
.net-toggle__enable:disabled,
.net-toggle__cancel:disabled { opacity: 0.5; cursor: not-allowed; }
+19 -3
View File
@@ -11,6 +11,7 @@ export default function NetworkToggle() {
const [st, setSt] = useState({ enabled: false });
const [busy, setBusy] = useState(false);
const [open, setOpen] = useState(false);
const [confirming, setConfirming] = useState(false);
const [qrs, setQrs] = useState({});
const refresh = useCallback(async () => {
@@ -31,10 +32,11 @@ export default function NetworkToggle() {
return () => { cancelled = true; };
}, [st.enabled, st.pin, st.share_port, st.lan_addresses]);
// NOTE: do NOT use window.confirm here — it's a no-op in the Tauri webview
// (returns false), which silently swallowed the enable action.
const enable = async () => {
if (!window.confirm('Share OmniVoice on your local network? Other devices will be able to reach it with the access PIN.')) return;
setBusy(true);
try { setSt(await apiPost('/system/network/enable')); setOpen(true); }
try { setSt(await apiPost('/system/network/enable')); setConfirming(false); setOpen(true); }
catch (e) { toast.error(`Could not enable sharing: ${e.message}`); }
finally { setBusy(false); }
};
@@ -51,7 +53,7 @@ export default function NetworkToggle() {
<div className="net-toggle">
<button
className={`net-toggle__pill ${st.enabled ? 'net-toggle__pill--on' : ''}`}
onClick={st.enabled ? () => setOpen((o) => !o) : enable}
onClick={st.enabled ? () => setOpen((o) => !o) : () => setConfirming((c) => !c)}
disabled={busy}
title={st.enabled ? 'Sharing on — click for details' : 'Share on your network'}
>
@@ -59,6 +61,20 @@ export default function NetworkToggle() {
<span>{busy ? 'Switching…' : st.enabled ? 'Network' : 'Local'}</span>
</button>
{!st.enabled && confirming && (
<div className="net-toggle__panel net-toggle__panel--confirm">
<div className="net-toggle__panel-title">Share on your network?</div>
<p className="net-toggle__hint">
Other devices on your Wi-Fi/Ethernet will be able to reach OmniVoice
using the access PIN shown once it's on.
</p>
<div className="net-toggle__confirm-actions">
<button type="button" className="net-toggle__cancel" onClick={() => setConfirming(false)} disabled={busy}>Cancel</button>
<button type="button" className="net-toggle__enable" onClick={enable} disabled={busy}>{busy ? 'Enabling' : 'Enable'}</button>
</div>
</div>
)}
{st.enabled && open && (
<div className="net-toggle__panel">
<div className="net-toggle__panel-title">Shared on your network</div>
+26 -1
View File
@@ -1,6 +1,6 @@
// frontend/src/components/NetworkToggle.test.jsx
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import NetworkToggle from './NetworkToggle';
describe('NetworkToggle', () => {
@@ -13,4 +13,29 @@ describe('NetworkToggle', () => {
render(<NetworkToggle />);
await waitFor(() => expect(screen.getByText(/local/i)).toBeInTheDocument());
});
it('Local → in-app confirm → Enable calls the enable endpoint (regression: no window.confirm)', async () => {
const posts = [];
global.fetch = vi.fn((url, opts) => {
const u = String(url);
if ((opts?.method || 'GET') === 'POST') posts.push(u);
if (u.endsWith('/system/network/enable')) {
return Promise.resolve({
ok: true,
json: async () => ({ enabled: true, share_port: 5050, pin: '123456', lan_addresses: [] }),
});
}
return Promise.resolve({ ok: true, json: async () => ({ enabled: false }) });
});
render(<NetworkToggle />);
const pill = await screen.findByRole('button', { name: /local/i });
fireEvent.click(pill); // opens the in-app confirm — must NOT depend on window.confirm
const enableBtn = await screen.findByRole('button', { name: /^enable$/i });
fireEvent.click(enableBtn);
await waitFor(() =>
expect(posts.some((u) => u.endsWith('/system/network/enable'))).toBe(true),
);
});
});