Improve wifi setup reliability and error handling
This commit is contained in:
parent
7073cab8b5
commit
6b6d29ef38
|
|
@ -63,14 +63,16 @@ BUNDLED_APPS="clock code cron env stats versions"
|
||||||
for app in $BUNDLED_APPS; do
|
for app in $BUNDLED_APPS; do
|
||||||
if [ -d "apps/$app" ]; then
|
if [ -d "apps/$app" ]; then
|
||||||
echo " Installing $app..."
|
echo " Installing $app..."
|
||||||
# Copy app to ~/apps
|
|
||||||
cp -r "apps/$app" ~/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)
|
version_dir=$(ls -1 ~/apps/$app | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r | head -1)
|
||||||
if [ -n "$version_dir" ]; then
|
if [ -n "$version_dir" ]; then
|
||||||
ln -sfn "$version_dir" ~/apps/$app/current
|
ln -sfn "$version_dir" ~/apps/$app/current
|
||||||
# Install dependencies
|
if ! (cd ~/apps/$app/current && bun install --frozen-lockfile) > /dev/null 2>&1; then
|
||||||
(cd ~/apps/$app/current && bun install --frozen-lockfile) > /dev/null 2>&1
|
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
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
@ -119,7 +121,6 @@ EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ">> Done! Rebooting in 5 seconds..."
|
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
|
sleep 5
|
||||||
quiet sudo nohup reboot >/dev/null 2>&1 &
|
sudo reboot
|
||||||
exit 0
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,3 @@ no-resolv
|
||||||
|
|
||||||
# Don't read /etc/hosts
|
# Don't read /etc/hosts
|
||||||
no-hosts
|
no-hosts
|
||||||
|
|
||||||
# Log queries for debugging
|
|
||||||
log-queries
|
|
||||||
log-facility=/tmp/toes-dnsmasq.log
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import {
|
||||||
import { theme } from '../themes'
|
import { theme } from '../themes'
|
||||||
import type { WifiNetwork } from '../../shared/types'
|
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) {
|
function signalBars(signal: number) {
|
||||||
const level = signal > 75 ? 4 : signal > 50 ? 3 : signal > 25 ? 2 : 1
|
const level = signal > 75 ? 4 : signal > 50 ? 3 : signal > 25 ? 2 : 1
|
||||||
|
|
@ -209,7 +209,6 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
<SpinnerWrap>
|
<SpinnerWrap>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<p style={{ color: theme('colors-textMuted') }}>Scanning for networks...</p>
|
<p style={{ color: theme('colors-textMuted') }}>Scanning for networks...</p>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
||||||
</SpinnerWrap>
|
</SpinnerWrap>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -257,7 +256,6 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
<SpinnerWrap>
|
<SpinnerWrap>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<p style={{ color: theme('colors-textMuted') }}>Connecting to <strong>{selectedNetwork?.ssid}</strong>...</p>
|
<p style={{ color: theme('colors-textMuted') }}>Connecting to <strong>{selectedNetwork?.ssid}</strong>...</p>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
||||||
</SpinnerWrap>
|
</SpinnerWrap>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,21 +51,9 @@ getWifiStatus().then(status => {
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).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
|
// SSE connection for app state
|
||||||
const events = new EventSource('/api/apps/stream')
|
const events = new EventSource('/api/apps/stream')
|
||||||
events.onmessage = e => {
|
events.onmessage = e => {
|
||||||
const prev = apps
|
|
||||||
setApps(JSON.parse(e.data))
|
setApps(JSON.parse(e.data))
|
||||||
|
|
||||||
if (selectedApp && !apps.some(a => a.name === selectedApp)) {
|
if (selectedApp && !apps.some(a => a.name === selectedApp)) {
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,10 @@ export const WifiColumn = define('WifiColumn', {
|
||||||
gap: 16,
|
gap: 16,
|
||||||
maxWidth: 400,
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { TOES_URL } from '$apps'
|
import { TOES_URL } from '$apps'
|
||||||
import { Hype } from '@because/hype'
|
import { Hype } from '@because/hype'
|
||||||
import { connectToWifi, getWifiStatus, isSetupMode, onSetupModeChange, scanNetworks } from '../wifi'
|
import { connectToWifi, getWifiStatus, isSetupMode, scanNetworks } from '../wifi'
|
||||||
|
|
||||||
const router = Hype.router()
|
const router = Hype.router()
|
||||||
|
|
||||||
|
|
@ -28,11 +28,4 @@ router.post('/connect', async c => {
|
||||||
return c.json(result)
|
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
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ async function dnsStart(): Promise<void> {
|
||||||
async function dnsStop(): Promise<void> {
|
async function dnsStop(): Promise<void> {
|
||||||
if (await Bun.file(DNSMASQ_PID).exists()) {
|
if (await Bun.file(DNSMASQ_PID).exists()) {
|
||||||
const pid = (await Bun.file(DNSMASQ_PID).text()).trim()
|
const pid = (await Bun.file(DNSMASQ_PID).text()).trim()
|
||||||
await sudo(['kill', pid]).catch(() => {})
|
await sudo(['kill', pid]).catch(e => hostLog(`dnsStop: failed to kill pid ${pid}: ${e}`))
|
||||||
await sudo(['rm', '-f', DNSMASQ_PID]).catch(() => {})
|
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(() => {})
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
// Connection failed — restart hotspot so user can retry
|
// Connection failed — restart hotspot so user can retry
|
||||||
|
try {
|
||||||
await startHotspot()
|
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'
|
const error = result.stderr || result.stdout || 'Connection failed'
|
||||||
return { ok: false, error }
|
return { ok: false, error }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,20 @@ import type { ConnectResult, WifiNetwork, WifiStatus } from '@types'
|
||||||
export type { ConnectResult, WifiNetwork, WifiStatus } from '@types'
|
export type { ConnectResult, WifiNetwork, WifiStatus } from '@types'
|
||||||
|
|
||||||
let _setupMode = false
|
let _setupMode = false
|
||||||
const _listeners = new Set<(setupMode: boolean) => void>()
|
|
||||||
|
|
||||||
export const isSetupMode = () => _setupMode
|
export const isSetupMode = () => _setupMode
|
||||||
|
|
||||||
export const onSetupModeChange = (cb: (setupMode: boolean) => void) => {
|
|
||||||
_listeners.add(cb)
|
|
||||||
return () => _listeners.delete(cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSetupMode(mode: boolean) {
|
function setSetupMode(mode: boolean) {
|
||||||
if (_setupMode === mode) return
|
if (_setupMode === mode) return
|
||||||
_setupMode = mode
|
_setupMode = mode
|
||||||
hostLog(mode ? 'Entering WiFi setup mode' : 'Exiting WiFi setup mode')
|
hostLog(mode ? 'Entering WiFi setup mode' : 'Exiting WiFi setup mode')
|
||||||
for (const cb of _listeners) cb(mode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWifiStatus(): Promise<WifiStatus> {
|
export async function getWifiStatus(): Promise<WifiStatus> {
|
||||||
try {
|
try {
|
||||||
return await nmcli.status()
|
return await nmcli.status()
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
hostLog(`WiFi status check failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||||
return { connected: false, ssid: '', ip: '' }
|
return { connected: false, ssid: '', ip: '' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +26,8 @@ export async function getWifiStatus(): Promise<WifiStatus> {
|
||||||
export async function scanNetworks(): Promise<WifiNetwork[]> {
|
export async function scanNetworks(): Promise<WifiNetwork[]> {
|
||||||
try {
|
try {
|
||||||
return await nmcli.scanNetworks()
|
return await nmcli.scanNetworks()
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
hostLog(`WiFi scan failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user