toggle for wifi and switch-router on openwrt page
This commit is contained in:
parent
e497f8fed1
commit
2a6e624189
@ -223,6 +223,7 @@ impl RpcHandler {
|
||||
"network.list-interfaces" => self.handle_network_list_interfaces().await,
|
||||
"network.scan-wifi" => self.handle_network_scan_wifi().await,
|
||||
"network.configure-wifi" => self.handle_network_configure_wifi(params).await,
|
||||
"network.set-wifi-radio" => self.handle_network_set_wifi_radio(params).await,
|
||||
"network.configure-ethernet" => self.handle_network_configure_ethernet(params).await,
|
||||
"network.dns-status" => self.handle_network_dns_status().await,
|
||||
"network.configure-dns" => self.handle_network_configure_dns(params).await,
|
||||
|
||||
@ -18,6 +18,24 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({ "networks": networks }))
|
||||
}
|
||||
|
||||
/// network.set-wifi-radio — turn the wifi adapter fully on or off (not just
|
||||
/// disconnect from a network). Params: `{ "enabled": bool }`.
|
||||
pub(super) async fn handle_network_set_wifi_radio(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let enabled = params
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: enabled"))?;
|
||||
|
||||
tracing::info!(enabled, "Setting wifi radio state");
|
||||
set_wifi_radio(enabled).await?;
|
||||
|
||||
Ok(serde_json::json!({ "ok": true, "enabled": enabled }))
|
||||
}
|
||||
|
||||
/// network.configure-wifi — connect to a WiFi network.
|
||||
pub(super) async fn handle_network_configure_wifi(
|
||||
&self,
|
||||
@ -327,6 +345,27 @@ fn split_nmcli_escaped(line: &str, limit: usize) -> Vec<String> {
|
||||
fields
|
||||
}
|
||||
|
||||
/// Turn the wifi radio fully on or off using nmcli (a rfkill-level toggle, not
|
||||
/// just disconnecting from the current network — the adapter stops scanning/
|
||||
/// associating entirely until switched back on).
|
||||
async fn set_wifi_radio(enabled: bool) -> Result<()> {
|
||||
let state = if enabled { "on" } else { "off" };
|
||||
let output = tokio::process::Command::new("nmcli")
|
||||
.args(["radio", "wifi", state])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run nmcli radio wifi")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"nmcli radio wifi {} failed: {}",
|
||||
state,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Connect to a WiFi network using nmcli.
|
||||
async fn connect_wifi(ssid: &str, password: &str) -> Result<()> {
|
||||
let conn_name = format!("archipelago-wifi-{ssid}");
|
||||
|
||||
@ -254,12 +254,28 @@
|
||||
<p class="text-xs text-white/50">{{ iface.type === 'wifi' ? 'WiFi' : 'Ethernet' }} · {{ 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'
|
||||
|
||||
@ -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 -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user