Improve wifi setup reliability and error handling

This commit is contained in:
Chris Wanstrath 2026-02-24 20:19:53 -08:00
parent 7073cab8b5
commit 6b6d29ef38
8 changed files with 28 additions and 46 deletions

View File

@ -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

View File

@ -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

View File

@ -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 }) {
<SpinnerWrap>
<Spinner />
<p style={{ color: theme('colors-textMuted') }}>Scanning for networks...</p>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</SpinnerWrap>
)}
@ -257,7 +256,6 @@ export function SettingsPage({ render }: { render: () => void }) {
<SpinnerWrap>
<Spinner />
<p style={{ color: theme('colors-textMuted') }}>Connecting to <strong>{selectedNetwork?.ssid}</strong>...</p>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</SpinnerWrap>
)}

View File

@ -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)) {

View File

@ -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)
}

View File

@ -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

View File

@ -16,8 +16,8 @@ async function dnsStart(): Promise<void> {
async function dnsStop(): Promise<void> {
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 }
}

View File

@ -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<WifiStatus> {
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<WifiStatus> {
export async function scanNetworks(): Promise<WifiNetwork[]> {
try {
return await nmcli.scanNetworks()
} catch {
} catch (e) {
hostLog(`WiFi scan failed: ${e instanceof Error ? e.message : String(e)}`)
return []
}
}