From 6b6d29ef38fb426d4189c07fa1c4363268f72b5b Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 24 Feb 2026 20:19:53 -0800 Subject: [PATCH] Improve wifi setup reliability and error handling --- scripts/install.sh | 15 ++++++++------- scripts/wifi-captive.conf | 4 ---- src/client/components/SettingsPage.tsx | 4 +--- src/client/index.tsx | 12 ------------ src/client/styles/wifi.ts | 7 +++++++ src/server/api/wifi.ts | 9 +-------- src/server/wifi-nmcli.ts | 10 +++++++--- src/server/wifi.ts | 13 ++++--------- 8 files changed, 28 insertions(+), 46 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index b020230..25c6e7b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -63,14 +63,16 @@ BUNDLED_APPS="clock code cron env stats versions" for app in $BUNDLED_APPS; do if [ -d "apps/$app" ]; then echo " Installing $app..." - # Copy app to ~/apps cp -r "apps/$app" ~/apps/ - # Find the version directory and create current symlink version_dir=$(ls -1 ~/apps/$app | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r | head -1) if [ -n "$version_dir" ]; then ln -sfn "$version_dir" ~/apps/$app/current - # Install dependencies - (cd ~/apps/$app/current && bun install --frozen-lockfile) > /dev/null 2>&1 + if ! (cd ~/apps/$app/current && bun install --frozen-lockfile) > /dev/null 2>&1; then + echo " WARNING: bun install failed for $app, trying without lockfile..." + (cd ~/apps/$app/current && bun install) > /dev/null 2>&1 || echo " ERROR: bun install failed for $app" + fi + else + echo " WARNING: no version directory found for $app, skipping" fi fi done @@ -119,7 +121,6 @@ EOF fi echo ">> Done! Rebooting in 5 seconds..." -quiet systemctl status "$SERVICE_NAME" --no-pager -l || true +systemctl status "$SERVICE_NAME" --no-pager -l || true sleep 5 -quiet sudo nohup reboot >/dev/null 2>&1 & -exit 0 +sudo reboot diff --git a/scripts/wifi-captive.conf b/scripts/wifi-captive.conf index 129f5b2..f9ce5a6 100644 --- a/scripts/wifi-captive.conf +++ b/scripts/wifi-captive.conf @@ -13,7 +13,3 @@ no-resolv # Don't read /etc/hosts no-hosts - -# Log queries for debugging -log-queries -log-facility=/tmp/toes-dnsmasq.log diff --git a/src/client/components/SettingsPage.tsx b/src/client/components/SettingsPage.tsx index 0ac90c6..3bc401d 100644 --- a/src/client/components/SettingsPage.tsx +++ b/src/client/components/SettingsPage.tsx @@ -33,7 +33,7 @@ import { import { theme } from '../themes' import type { WifiNetwork } from '../../shared/types' -type WifiStep = 'status' | 'scanning' | 'networks' | 'password' | 'connecting' | 'success' | 'error' +type WifiStep = 'status' | 'scanning' | 'networks' | 'password' | 'connecting' | 'success' function signalBars(signal: number) { const level = signal > 75 ? 4 : signal > 50 ? 3 : signal > 25 ? 2 : 1 @@ -209,7 +209,6 @@ export function SettingsPage({ render }: { render: () => void }) {

Scanning for networks...

-
)} @@ -257,7 +256,6 @@ export function SettingsPage({ render }: { render: () => void }) {

Connecting to {selectedNetwork?.ssid}...

-
)} diff --git a/src/client/index.tsx b/src/client/index.tsx index ad9f832..a2280e0 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -51,21 +51,9 @@ getWifiStatus().then(status => { } }).catch(() => {}) -// SSE for WiFi setup mode changes -const wifiEvents = new EventSource('/api/wifi/stream') -wifiEvents.onmessage = e => { - const data = JSON.parse(e.data) - setSetupMode(data.setupMode) - if (data.setupMode) { - setCurrentView('settings') - } - render() -} - // SSE connection for app state const events = new EventSource('/api/apps/stream') events.onmessage = e => { - const prev = apps setApps(JSON.parse(e.data)) if (selectedApp && !apps.some(a => a.name === selectedApp)) { diff --git a/src/client/styles/wifi.ts b/src/client/styles/wifi.ts index 623fbab..8b498b5 100644 --- a/src/client/styles/wifi.ts +++ b/src/client/styles/wifi.ts @@ -95,3 +95,10 @@ export const WifiColumn = define('WifiColumn', { gap: 16, maxWidth: 400, }) + +// Inject spin keyframes once +if (typeof document !== 'undefined') { + const style = document.createElement('style') + style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }' + document.head.appendChild(style) +} diff --git a/src/server/api/wifi.ts b/src/server/api/wifi.ts index 2130f70..4520d4b 100644 --- a/src/server/api/wifi.ts +++ b/src/server/api/wifi.ts @@ -1,6 +1,6 @@ import { TOES_URL } from '$apps' import { Hype } from '@because/hype' -import { connectToWifi, getWifiStatus, isSetupMode, onSetupModeChange, scanNetworks } from '../wifi' +import { connectToWifi, getWifiStatus, isSetupMode, scanNetworks } from '../wifi' const router = Hype.router() @@ -28,11 +28,4 @@ router.post('/connect', async c => { return c.json(result) }) -// SSE stream for setup mode changes -router.sse('/stream', (send, c) => { - send({ setupMode: isSetupMode() }) - const unsub = onSetupModeChange(setupMode => send({ setupMode })) - return () => unsub() -}) - export default router diff --git a/src/server/wifi-nmcli.ts b/src/server/wifi-nmcli.ts index 8d55248..6a122ab 100644 --- a/src/server/wifi-nmcli.ts +++ b/src/server/wifi-nmcli.ts @@ -16,8 +16,8 @@ async function dnsStart(): Promise { async function dnsStop(): Promise { if (await Bun.file(DNSMASQ_PID).exists()) { const pid = (await Bun.file(DNSMASQ_PID).text()).trim() - await sudo(['kill', pid]).catch(() => {}) - await sudo(['rm', '-f', DNSMASQ_PID]).catch(() => {}) + await sudo(['kill', pid]).catch(e => hostLog(`dnsStop: failed to kill pid ${pid}: ${e}`)) + await sudo(['rm', '-f', DNSMASQ_PID]).catch(e => hostLog(`dnsStop: failed to remove pid file: ${e}`)) } await sudo(['pkill', '-f', 'dnsmasq.*wifi-captive.conf']).catch(() => {}) } @@ -64,7 +64,11 @@ export async function connectToNetwork(ssid: string, password?: string): Promise if (result.exitCode !== 0) { // Connection failed — restart hotspot so user can retry - await startHotspot() + try { + await startHotspot() + } catch (e) { + hostLog(`CRITICAL: failed to restart hotspot after connection failure: ${e instanceof Error ? e.message : String(e)}`) + } const error = result.stderr || result.stdout || 'Connection failed' return { ok: false, error } } diff --git a/src/server/wifi.ts b/src/server/wifi.ts index b9c3c92..285f256 100644 --- a/src/server/wifi.ts +++ b/src/server/wifi.ts @@ -5,26 +5,20 @@ import type { ConnectResult, WifiNetwork, WifiStatus } from '@types' export type { ConnectResult, WifiNetwork, WifiStatus } from '@types' let _setupMode = false -const _listeners = new Set<(setupMode: boolean) => void>() export const isSetupMode = () => _setupMode -export const onSetupModeChange = (cb: (setupMode: boolean) => void) => { - _listeners.add(cb) - return () => _listeners.delete(cb) -} - function setSetupMode(mode: boolean) { if (_setupMode === mode) return _setupMode = mode hostLog(mode ? 'Entering WiFi setup mode' : 'Exiting WiFi setup mode') - for (const cb of _listeners) cb(mode) } export async function getWifiStatus(): Promise { try { return await nmcli.status() - } catch { + } catch (e) { + hostLog(`WiFi status check failed: ${e instanceof Error ? e.message : String(e)}`) return { connected: false, ssid: '', ip: '' } } } @@ -32,7 +26,8 @@ export async function getWifiStatus(): Promise { export async function scanNetworks(): Promise { try { return await nmcli.scanNetworks() - } catch { + } catch (e) { + hostLog(`WiFi scan failed: ${e instanceof Error ? e.message : String(e)}`) return [] } }