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.list-interfaces" => self.handle_network_list_interfaces().await,
|
||||||
"network.scan-wifi" => self.handle_network_scan_wifi().await,
|
"network.scan-wifi" => self.handle_network_scan_wifi().await,
|
||||||
"network.configure-wifi" => self.handle_network_configure_wifi(params).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.configure-ethernet" => self.handle_network_configure_ethernet(params).await,
|
||||||
"network.dns-status" => self.handle_network_dns_status().await,
|
"network.dns-status" => self.handle_network_dns_status().await,
|
||||||
"network.configure-dns" => self.handle_network_configure_dns(params).await,
|
"network.configure-dns" => self.handle_network_configure_dns(params).await,
|
||||||
|
|||||||
@ -18,6 +18,24 @@ impl RpcHandler {
|
|||||||
Ok(serde_json::json!({ "networks": networks }))
|
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.
|
/// network.configure-wifi — connect to a WiFi network.
|
||||||
pub(super) async fn handle_network_configure_wifi(
|
pub(super) async fn handle_network_configure_wifi(
|
||||||
&self,
|
&self,
|
||||||
@ -327,6 +345,27 @@ fn split_nmcli_escaped(line: &str, limit: usize) -> Vec<String> {
|
|||||||
fields
|
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.
|
/// Connect to a WiFi network using nmcli.
|
||||||
async fn connect_wifi(ssid: &str, password: &str) -> Result<()> {
|
async fn connect_wifi(ssid: &str, password: &str) -> Result<()> {
|
||||||
let conn_name = format!("archipelago-wifi-{ssid}");
|
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>
|
<p class="text-xs text-white/50">{{ iface.type === 'wifi' ? 'WiFi' : 'Ethernet' }} · {{ iface.mac }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p v-if="iface.ipv4.length > 0" class="text-sm text-white/80">{{ iface.ipv4[0] }}</p>
|
<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>
|
<p v-else class="text-sm text-white/40">No IP</p>
|
||||||
</div>
|
</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>
|
</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="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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -568,6 +584,9 @@ const allInterfaces = ref<NetworkInterface[]>([])
|
|||||||
const physicalInterfaces = computed(() => allInterfaces.value.filter(i => i.type === 'ethernet' || i.type === 'wifi'))
|
const physicalInterfaces = computed(() => allInterfaces.value.filter(i => i.type === 'ethernet' || i.type === 'wifi'))
|
||||||
const wifiAvailable = computed(() => allInterfaces.value.some(i => 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 showWifiModal = ref(false)
|
||||||
const wifiScanning = ref(false)
|
const wifiScanning = ref(false)
|
||||||
const wifiNetworks = ref<WifiNetwork[]>([])
|
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 }
|
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 {
|
function wifiRequiresPassword(network: WifiNetwork | undefined): boolean {
|
||||||
const security = (network?.security || '').trim().toLowerCase()
|
const security = (network?.security || '').trim().toLowerCase()
|
||||||
return security.length > 0 && security !== '--' && security !== 'none' && security !== 'open'
|
return security.length > 0 && security !== '--' && security !== 'none' && security !== 'open'
|
||||||
|
|||||||
@ -77,6 +77,10 @@ const showConnectForm = ref(false)
|
|||||||
const connecting = ref(false)
|
const connecting = ref(false)
|
||||||
const connectedParams = ref<Record<string, string> | null>(null)
|
const connectedParams = ref<Record<string, string> | null>(null)
|
||||||
|
|
||||||
|
const detecting = ref(false)
|
||||||
|
const detectError = ref('')
|
||||||
|
const detectedCandidates = ref<string[]>([])
|
||||||
|
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
const provisionError = ref('')
|
const provisionError = ref('')
|
||||||
const provisionSuccess = ref(false)
|
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() {
|
async function provisionTollgate() {
|
||||||
provisioning.value = true
|
provisioning.value = true
|
||||||
provisionError.value = ''
|
provisionError.value = ''
|
||||||
@ -314,12 +373,34 @@ onMounted(() => load())
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-white/40 mb-1">Router IP</label>
|
<label class="block text-xs text-white/40 mb-1">Router IP</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="host"
|
v-model="host"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="192.168.1.1"
|
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"
|
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>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
@ -403,9 +484,14 @@ onMounted(() => load())
|
|||||||
<dd class="text-white/70">{{ formatUptime(status.uptime_secs) }}</dd>
|
<dd class="text-white/70">{{ formatUptime(status.uptime_secs) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<button class="mt-4 text-xs text-white/40 hover:text-white/70 transition-colors" @click="load()">
|
<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
|
↻ Refresh
|
||||||
</button>
|
</button>
|
||||||
|
<button class="text-xs text-white/40 hover:text-white/70 transition-colors" @click="disconnectRouter">
|
||||||
|
Switch router →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- WAN / Uplink -->
|
<!-- WAN / Uplink -->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user