toggle for wifi and switch-router on openwrt page

This commit is contained in:
2026-07-01 14:06:02 +00:00
parent e497f8fed1
commit 2a6e624189
4 changed files with 171 additions and 12 deletions
+36 -3
View File
@@ -254,12 +254,28 @@
<p class="text-xs text-white/50">{{ iface.type === 'wifi' ? 'WiFi' : 'Ethernet' }} &middot; {{ iface.mac }}</p>
</div>
</div>
<div class="text-right">
<p v-if="iface.ipv4.length > 0" class="text-sm text-white/80">{{ iface.ipv4[0] }}</p>
<p v-else class="text-sm text-white/40">No IP</p>
<div class="flex items-center gap-3">
<div class="text-right">
<p v-if="iface.ipv4.length > 0" class="text-sm text-white/80">{{ iface.ipv4[0] }}</p>
<p v-else class="text-sm text-white/40">No IP</p>
</div>
<button
v-if="iface.type === 'wifi'"
:disabled="togglingWifiRadio"
class="relative w-11 h-6 rounded-full transition-colors flex-shrink-0"
:class="[iface.state === 'up' ? 'bg-green-500/60' : 'bg-white/15', togglingWifiRadio ? 'opacity-40 cursor-not-allowed' : '']"
:aria-label="iface.state === 'up' ? 'Turn off wifi adapter' : 'Turn on wifi adapter'"
@click="toggleWifiRadio(iface)"
>
<span
class="absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform"
:class="iface.state === 'up' ? 'translate-x-5' : 'translate-x-0.5'"
></span>
</button>
</div>
</div>
<p v-if="physicalInterfaces.length === 0" class="text-sm text-white/50 text-center py-4">No physical interfaces detected</p>
<p v-if="wifiRadioError" class="text-xs text-red-400">{{ wifiRadioError }}</p>
</div>
</template>
@@ -568,6 +584,9 @@ const allInterfaces = ref<NetworkInterface[]>([])
const physicalInterfaces = computed(() => allInterfaces.value.filter(i => i.type === 'ethernet' || i.type === 'wifi'))
const wifiAvailable = computed(() => allInterfaces.value.some(i => i.type === 'wifi'))
const togglingWifiRadio = ref(false)
const wifiRadioError = ref('')
const showWifiModal = ref(false)
const wifiScanning = ref(false)
const wifiNetworks = ref<WifiNetwork[]>([])
@@ -623,6 +642,20 @@ async function loadInterfaces() {
try { const res = await rpcClient.call<{ interfaces: NetworkInterface[] }>({ method: 'network.list-interfaces' }); allInterfaces.value = res.interfaces } catch { if (!hadInterfaces) allInterfaces.value = [] } finally { interfacesHaveLoaded.value = true; interfacesLoading.value = false; interfacesRefreshing.value = false }
}
async function toggleWifiRadio(iface: NetworkInterface) {
togglingWifiRadio.value = true
wifiRadioError.value = ''
const enabled = iface.state !== 'up'
try {
await rpcClient.call({ method: 'network.set-wifi-radio', params: { enabled } })
await loadInterfaces()
} catch (e) {
wifiRadioError.value = e instanceof Error ? e.message : 'Failed to change wifi radio state.'
} finally {
togglingWifiRadio.value = false
}
}
function wifiRequiresPassword(network: WifiNetwork | undefined): boolean {
const security = (network?.security || '').trim().toLowerCase()
return security.length > 0 && security !== '--' && security !== 'none' && security !== 'open'
+95 -9
View File
@@ -77,6 +77,10 @@ const showConnectForm = ref(false)
const connecting = ref(false)
const connectedParams = ref<Record<string, string> | null>(null)
const detecting = ref(false)
const detectError = ref('')
const detectedCandidates = ref<string[]>([])
const provisioning = ref(false)
const provisionError = ref('')
const provisionSuccess = ref(false)
@@ -138,6 +142,61 @@ async function connect() {
}
}
interface WiredInterface { name: string; type: string; state: string; ipv4: string[] }
async function detectRouter() {
detecting.value = true
detectError.value = ''
detectedCandidates.value = []
try {
const { interfaces } = await rpcClient.call<{ interfaces: WiredInterface[] }>({
method: 'network.list-interfaces',
timeout: 10000,
})
const wired = interfaces.find(i => i.type === 'ethernet' && i.state === 'up' && i.ipv4.length > 0)
if (!wired) {
detectError.value = 'No active wired ethernet connection found on this node.'
return
}
const [ip, prefixStr] = wired.ipv4[0]!.split('/')
const prefix = Number(prefixStr) || 24
const { routers } = await rpcClient.call<{ routers: string[] }>({
method: 'openwrt.scan',
params: { subnet: ip, prefix, ssh_user: sshUser.value, ssh_password: sshPassword.value },
timeout: 120000,
})
if (routers.length === 0) {
detectError.value = `No OpenWrt router found on ${wired.name}'s network (/${prefix}).`
} else if (routers.length === 1) {
host.value = routers[0]!
} else {
detectedCandidates.value = routers
}
} catch (e) {
detectError.value = e instanceof Error ? e.message : String(e)
} finally {
detecting.value = false
}
}
function pickDetectedRouter(ip: string) {
host.value = ip
detectedCandidates.value = []
}
// The router config now persists server-side (see handle_openwrt_get_status),
// so onMounted's no-args load() always reconnects automatically — without this,
// there'd be no way back to the connect form (and its Detect button) at all.
function disconnectRouter() {
host.value = status.value?.host ?? host.value
connectedParams.value = null
detectError.value = ''
detectedCandidates.value = []
showConnectForm.value = true
}
async function provisionTollgate() {
provisioning.value = true
provisionError.value = ''
@@ -314,12 +373,34 @@ onMounted(() => load())
<div class="space-y-3">
<div>
<label class="block text-xs text-white/40 mb-1">Router IP</label>
<input
v-model="host"
type="text"
placeholder="192.168.1.1"
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
/>
<div class="flex items-center gap-2">
<input
v-model="host"
type="text"
placeholder="192.168.1.1"
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
/>
<button
:disabled="detecting"
class="glass-button px-4 py-3 text-sm font-medium whitespace-nowrap flex-shrink-0"
:class="detecting ? 'opacity-40 cursor-not-allowed' : ''"
@click="detectRouter"
>
{{ detecting ? 'Detecting…' : 'Detect' }}
</button>
</div>
<p v-if="detectError" class="mt-2 text-xs text-red-400">{{ detectError }}</p>
<div v-if="detectedCandidates.length > 0" class="mt-2 space-y-1">
<p class="text-xs text-white/40">Multiple routers found pick one:</p>
<button
v-for="ip in detectedCandidates"
:key="ip"
class="w-full text-left px-3 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-sm text-white font-mono transition-colors"
@click="pickDetectedRouter(ip)"
>
{{ ip }}
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
@@ -403,9 +484,14 @@ onMounted(() => load())
<dd class="text-white/70">{{ formatUptime(status.uptime_secs) }}</dd>
</div>
</dl>
<button class="mt-4 text-xs text-white/40 hover:text-white/70 transition-colors" @click="load()">
Refresh
</button>
<div class="mt-4 flex items-center gap-4">
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="load()">
Refresh
</button>
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="disconnectRouter">
Switch router
</button>
</div>
</div>
<!-- WAN / Uplink -->