2026-06-28 19:28:25 +00:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
|
|
|
|
import BackButton from '@/components/BackButton.vue'
|
|
|
|
|
|
|
|
|
|
|
|
interface ReleaseInfo {
|
|
|
|
|
|
openwrt_release?: string
|
|
|
|
|
|
openwrt_version?: string
|
|
|
|
|
|
openwrt_board_name?: string
|
|
|
|
|
|
openwrt_arch?: string
|
|
|
|
|
|
openwrt_target?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface WifiInterface {
|
|
|
|
|
|
section: string
|
|
|
|
|
|
ssid: string
|
|
|
|
|
|
device: string
|
|
|
|
|
|
encryption: string
|
|
|
|
|
|
network: string
|
|
|
|
|
|
disabled: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TollGateStatus {
|
|
|
|
|
|
installed: boolean
|
|
|
|
|
|
enabled?: boolean
|
|
|
|
|
|
metric?: string
|
|
|
|
|
|
step_size_ms?: number
|
|
|
|
|
|
price_per_step?: number
|
|
|
|
|
|
currency?: string
|
|
|
|
|
|
mint_url?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
interface WanStatus {
|
|
|
|
|
|
configured: boolean
|
|
|
|
|
|
ssid: string
|
2026-06-29 18:46:58 +00:00
|
|
|
|
assoc_ssid: string
|
2026-06-29 15:11:48 +00:00
|
|
|
|
encryption: string
|
|
|
|
|
|
ip: string
|
|
|
|
|
|
internet: boolean
|
2026-06-29 18:46:58 +00:00
|
|
|
|
radio0_disabled: boolean
|
|
|
|
|
|
sta_iface: string
|
|
|
|
|
|
sta_state: string
|
|
|
|
|
|
wifi_log: string
|
2026-06-30 17:12:50 +00:00
|
|
|
|
lan_ip: string
|
|
|
|
|
|
lan_netmask: string
|
|
|
|
|
|
dhcp_start: string
|
|
|
|
|
|
dhcp_limit: string
|
|
|
|
|
|
masq: boolean
|
2026-06-29 15:11:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 19:28:25 +00:00
|
|
|
|
interface RouterStatus {
|
|
|
|
|
|
host: string
|
|
|
|
|
|
hostname: string
|
|
|
|
|
|
uptime_secs: number
|
|
|
|
|
|
release: ReleaseInfo
|
|
|
|
|
|
tollgate: TollGateStatus
|
|
|
|
|
|
wifi_interfaces: WifiInterface[]
|
2026-06-29 15:11:48 +00:00
|
|
|
|
wan: WanStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ScannedNetwork {
|
|
|
|
|
|
ssid: string
|
|
|
|
|
|
bssid: string
|
|
|
|
|
|
signal: number
|
|
|
|
|
|
channel: number
|
|
|
|
|
|
encryption: string
|
2026-06-28 19:28:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const status = ref<RouterStatus | null>(null)
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const error = ref('')
|
|
|
|
|
|
const host = ref('')
|
|
|
|
|
|
const sshUser = ref('root')
|
|
|
|
|
|
const sshPassword = ref('')
|
|
|
|
|
|
const showConnectForm = ref(false)
|
|
|
|
|
|
const connecting = ref(false)
|
2026-06-29 01:22:45 +00:00
|
|
|
|
const connectedParams = ref<Record<string, string> | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const provisioning = ref(false)
|
|
|
|
|
|
const provisionError = ref('')
|
|
|
|
|
|
const provisionSuccess = ref(false)
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
// WAN setup flow
|
2026-06-30 17:12:50 +00:00
|
|
|
|
type WanStep = 'idle' | 'scan' | 'scanning' | 'list' | 'password' | 'dhcp' | 'connecting' | 'done'
|
2026-06-29 15:11:48 +00:00
|
|
|
|
const wanStep = ref<WanStep>('idle')
|
|
|
|
|
|
const scannedNetworks = ref<ScannedNetwork[]>([])
|
|
|
|
|
|
const selectedNetwork = ref<ScannedNetwork | null>(null)
|
|
|
|
|
|
const wanPassword = ref('')
|
|
|
|
|
|
const wanError = ref('')
|
|
|
|
|
|
|
2026-06-30 17:12:50 +00:00
|
|
|
|
// DHCP/masq settings (step 3 of wizard)
|
|
|
|
|
|
const dhcpStart = ref(100)
|
|
|
|
|
|
const dhcpLimit = ref(150)
|
|
|
|
|
|
const masqEnabled = ref(true)
|
|
|
|
|
|
|
2026-06-28 19:28:25 +00:00
|
|
|
|
async function load(params?: Record<string, string>) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
status.value = await rpcClient.call<RouterStatus>({
|
|
|
|
|
|
method: 'openwrt.get-status',
|
|
|
|
|
|
params: params ?? {},
|
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
|
})
|
|
|
|
|
|
showConnectForm.value = false
|
2026-06-29 01:22:45 +00:00
|
|
|
|
if (params) connectedParams.value = params
|
2026-06-28 19:28:25 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
|
|
|
|
if (msg.includes('No router configured')) {
|
|
|
|
|
|
showConnectForm.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
error.value = msg
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function connect() {
|
|
|
|
|
|
if (!host.value.trim()) return
|
|
|
|
|
|
connecting.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
await load({ host: host.value.trim(), ssh_user: sshUser.value, ssh_password: sshPassword.value })
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
connecting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 01:22:45 +00:00
|
|
|
|
async function provisionTollgate() {
|
|
|
|
|
|
provisioning.value = true
|
|
|
|
|
|
provisionError.value = ''
|
|
|
|
|
|
provisionSuccess.value = false
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params: Record<string, unknown> = {
|
|
|
|
|
|
host: connectedParams.value?.host ?? status.value?.host,
|
|
|
|
|
|
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
|
|
|
|
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
|
|
|
|
}
|
2026-06-29 15:11:48 +00:00
|
|
|
|
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
2026-06-29 01:22:45 +00:00
|
|
|
|
provisionSuccess.value = true
|
|
|
|
|
|
await load(connectedParams.value ?? undefined)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
provisionError.value = e instanceof Error ? e.message : String(e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
provisioning.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
function startWanSetup() {
|
|
|
|
|
|
wanStep.value = 'scan'
|
|
|
|
|
|
wanError.value = ''
|
|
|
|
|
|
scannedNetworks.value = []
|
|
|
|
|
|
selectedNetwork.value = null
|
|
|
|
|
|
wanPassword.value = ''
|
2026-06-30 17:12:50 +00:00
|
|
|
|
dhcpStart.value = Number(status.value?.wan?.dhcp_start) || 100
|
|
|
|
|
|
dhcpLimit.value = Number(status.value?.wan?.dhcp_limit) || 150
|
|
|
|
|
|
masqEnabled.value = true
|
2026-06-29 15:11:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function scanWifi() {
|
|
|
|
|
|
wanStep.value = 'scanning'
|
|
|
|
|
|
wanError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params: Record<string, unknown> = {
|
|
|
|
|
|
host: connectedParams.value?.host ?? status.value?.host,
|
|
|
|
|
|
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
|
|
|
|
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({
|
|
|
|
|
|
method: 'openwrt.scan-wifi',
|
|
|
|
|
|
params,
|
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
|
})
|
|
|
|
|
|
scannedNetworks.value = result.networks
|
|
|
|
|
|
wanStep.value = 'list'
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
wanError.value = e instanceof Error ? e.message : String(e)
|
|
|
|
|
|
wanStep.value = 'scan'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectNetwork(net: ScannedNetwork) {
|
|
|
|
|
|
selectedNetwork.value = net
|
|
|
|
|
|
wanPassword.value = ''
|
|
|
|
|
|
wanError.value = ''
|
|
|
|
|
|
wanStep.value = 'password'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function configureWan() {
|
|
|
|
|
|
if (!selectedNetwork.value) return
|
|
|
|
|
|
wanStep.value = 'connecting'
|
|
|
|
|
|
wanError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params: Record<string, unknown> = {
|
|
|
|
|
|
host: connectedParams.value?.host ?? status.value?.host,
|
|
|
|
|
|
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
|
|
|
|
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
|
|
|
|
ssid: selectedNetwork.value.ssid,
|
|
|
|
|
|
password: wanPassword.value,
|
|
|
|
|
|
encryption: selectedNetwork.value.encryption,
|
2026-06-30 17:12:50 +00:00
|
|
|
|
dhcp_start: dhcpStart.value,
|
|
|
|
|
|
dhcp_limit: dhcpLimit.value,
|
|
|
|
|
|
masq: masqEnabled.value,
|
2026-06-29 15:11:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
await rpcClient.call({ method: 'openwrt.configure-wan', params, timeout: 30000 })
|
|
|
|
|
|
wanStep.value = 'done'
|
|
|
|
|
|
// Give the router ~8s to associate before reloading status
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
wanStep.value = 'idle'
|
|
|
|
|
|
load(connectedParams.value ?? undefined)
|
|
|
|
|
|
}, 8000)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
wanError.value = e instanceof Error ? e.message : String(e)
|
2026-06-30 17:12:50 +00:00
|
|
|
|
wanStep.value = 'dhcp'
|
2026-06-29 15:11:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function signalBars(dbm: number): number {
|
|
|
|
|
|
if (dbm >= -50) return 4
|
|
|
|
|
|
if (dbm >= -65) return 3
|
|
|
|
|
|
if (dbm >= -75) return 2
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 19:28:25 +00:00
|
|
|
|
function formatUptime(secs: number): string {
|
|
|
|
|
|
const d = Math.floor(secs / 86400)
|
|
|
|
|
|
const h = Math.floor((secs % 86400) / 3600)
|
|
|
|
|
|
const m = Math.floor((secs % 3600) / 60)
|
|
|
|
|
|
const parts = []
|
|
|
|
|
|
if (d) parts.push(`${d}d`)
|
|
|
|
|
|
if (h) parts.push(`${h}h`)
|
|
|
|
|
|
parts.push(`${m}m`)
|
|
|
|
|
|
return parts.join(' ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatStepSize(ms: number): string {
|
|
|
|
|
|
if (ms >= 3_600_000) return `${ms / 3_600_000}h`
|
|
|
|
|
|
if (ms >= 60_000) return `${ms / 60_000}m`
|
|
|
|
|
|
if (ms >= 1_000) return `${ms / 1_000}s`
|
|
|
|
|
|
return `${ms}ms`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openwrtVersion = computed(() => {
|
|
|
|
|
|
if (!status.value) return ''
|
|
|
|
|
|
const r = status.value.release
|
|
|
|
|
|
return r.openwrt_release || r.openwrt_version || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const boardName = computed(() => {
|
|
|
|
|
|
if (!status.value) return ''
|
|
|
|
|
|
return status.value.release.openwrt_board_name || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => load())
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="pb-6">
|
|
|
|
|
|
<div class="flex items-center gap-3 mb-6">
|
|
|
|
|
|
<BackButton />
|
|
|
|
|
|
<h1 class="text-lg font-semibold text-white">OpenWrt Gateway</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<!-- Connect form -->
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div v-if="showConnectForm" class="glass-card p-6 mb-6">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<h2 class="text-sm font-semibold text-white/80 mb-4">Connect to Router</h2>
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<div>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<label class="block text-xs text-white/40 mb-1">Router IP</label>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<input
|
|
|
|
|
|
v-model="host"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="192.168.1.1"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
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"
|
2026-06-28 19:28:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<label class="block text-xs text-white/40 mb-1">SSH User</label>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<input
|
|
|
|
|
|
v-model="sshUser"
|
|
|
|
|
|
type="text"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
2026-06-28 19:28:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<label class="block text-xs text-white/40 mb-1">Password</label>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<input
|
|
|
|
|
|
v-model="sshPassword"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="(blank for none)"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
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"
|
2026-06-28 19:28:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
:disabled="connecting || !host.trim()"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
class="glass-button w-full text-sm font-medium"
|
|
|
|
|
|
:class="connecting || !host.trim() ? 'opacity-40 cursor-not-allowed' : 'glass-button-warning'"
|
2026-06-28 19:28:25 +00:00
|
|
|
|
@click="connect"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ connecting ? 'Connecting…' : 'Connect' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p v-if="error" class="mt-3 text-xs text-red-400">{{ error }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Loading skeleton -->
|
|
|
|
|
|
<template v-else-if="loading">
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div v-for="i in 3" :key="i" class="glass-card p-6 mb-4 animate-pulse">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<div class="h-4 bg-white/10 rounded w-1/3 mb-4"></div>
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<div class="h-3 bg-white/10 rounded w-2/3"></div>
|
|
|
|
|
|
<div class="h-3 bg-white/10 rounded w-1/2"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error state -->
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div v-else-if="error" class="glass-card p-6 mb-4">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<p class="text-sm text-red-300">{{ error }}</p>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<button class="mt-3 text-xs text-white/50 hover:text-white transition-colors underline" @click="load()">Retry</button>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Status panels -->
|
|
|
|
|
|
<template v-else-if="status">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- System info -->
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div class="glass-card p-6 mb-4">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
|
<h2 class="text-sm font-semibold text-white/80">System</h2>
|
|
|
|
|
|
<span class="flex items-center gap-1.5 text-xs text-green-400">
|
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-green-400 inline-block"></span>
|
|
|
|
|
|
Connected
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Hostname</dt>
|
|
|
|
|
|
<dd class="text-white font-medium">{{ status.hostname }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Address</dt>
|
|
|
|
|
|
<dd class="text-white font-medium font-mono">{{ status.host }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="openwrtVersion">
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">OpenWrt</dt>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<dd class="text-white/70">{{ openwrtVersion }}</dd>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="boardName">
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Board</dt>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<dd class="text-white/70">{{ boardName }}</dd>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Uptime</dt>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<dd class="text-white/70">{{ formatUptime(status.uptime_secs) }}</dd>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<button class="mt-4 text-xs text-white/40 hover:text-white/70 transition-colors" @click="load()">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
↻ Refresh
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<!-- WAN / Uplink -->
|
|
|
|
|
|
<div class="glass-card p-6 mb-4">
|
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
|
<h2 class="text-sm font-semibold text-white/80">WAN / Uplink</h2>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="wanStep === 'idle' || wanStep === 'done'"
|
|
|
|
|
|
class="text-xs text-white/40 hover:text-white transition-colors"
|
|
|
|
|
|
@click="startWanSetup"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ status.wan?.configured ? 'Reconfigure →' : 'Set up →' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Current status (idle) -->
|
|
|
|
|
|
<template v-if="wanStep === 'idle' || wanStep === 'done'">
|
2026-06-29 18:46:58 +00:00
|
|
|
|
<div v-if="status.wan?.configured" class="flex items-center gap-3 mb-3">
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<span class="w-2 h-2 rounded-full inline-block flex-shrink-0"
|
|
|
|
|
|
:class="status.wan.internet ? 'bg-green-400' : 'bg-yellow-400 animate-pulse'"></span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-sm font-medium text-white">{{ status.wan.ssid }}</div>
|
|
|
|
|
|
<div class="text-xs text-white/40">
|
|
|
|
|
|
{{ status.wan.internet ? status.wan.ip : 'Connecting…' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-29 18:46:58 +00:00
|
|
|
|
<div v-else class="text-sm text-white/50 mb-3">
|
2026-06-29 15:11:48 +00:00
|
|
|
|
Not configured — router has no internet access.
|
|
|
|
|
|
</div>
|
2026-06-29 18:46:58 +00:00
|
|
|
|
|
2026-06-30 17:12:50 +00:00
|
|
|
|
<!-- Quick summary of DHCP + masq when connected -->
|
|
|
|
|
|
<dl v-if="status.wan?.configured && status.wan.internet"
|
|
|
|
|
|
class="grid grid-cols-3 gap-x-4 gap-y-1 text-xs text-white/40 mb-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/25">LAN</dt>
|
|
|
|
|
|
<dd class="text-white/60 font-mono">{{ status.wan.lan_ip }}/24</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/25">DHCP</dt>
|
|
|
|
|
|
<dd class="text-white/60 font-mono">.{{ status.wan.dhcp_start }}+{{ status.wan.dhcp_limit }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/25">NAT</dt>
|
|
|
|
|
|
<dd :class="status.wan.masq ? 'text-green-400' : 'text-red-400'">
|
|
|
|
|
|
{{ status.wan.masq ? 'on' : 'off' }}
|
|
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
2026-06-29 18:46:58 +00:00
|
|
|
|
<!-- Diagnostics (shown when not connected) -->
|
|
|
|
|
|
<dl v-if="status.wan?.configured && !status.wan.internet"
|
|
|
|
|
|
class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs mt-1 mb-3 border-t border-white/10 pt-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/30 mb-0.5">Radio</dt>
|
|
|
|
|
|
<dd :class="status.wan.radio0_disabled ? 'text-red-400' : 'text-green-400'">
|
|
|
|
|
|
{{ status.wan.radio0_disabled ? 'disabled' : 'enabled' }}
|
|
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/30 mb-0.5">Interface</dt>
|
|
|
|
|
|
<dd class="text-white/70 font-mono">
|
|
|
|
|
|
{{ status.wan.sta_iface || 'none' }}
|
|
|
|
|
|
<span v-if="status.wan.sta_iface" class="ml-1"
|
|
|
|
|
|
:class="status.wan.sta_state === 'up' ? 'text-green-400' : 'text-yellow-400'">
|
|
|
|
|
|
({{ status.wan.sta_state }})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/30 mb-0.5">Associated to</dt>
|
|
|
|
|
|
<dd class="text-white/70">{{ status.wan.assoc_ssid || 'none' }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-white/30 mb-0.5">Configured SSID</dt>
|
|
|
|
|
|
<dd class="text-white/70">{{ status.wan.ssid || '—' }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="status.wan.wifi_log" class="col-span-2">
|
|
|
|
|
|
<dt class="text-white/30 mb-1">Recent wifi log</dt>
|
|
|
|
|
|
<dd class="font-mono text-white/50 text-[10px] leading-relaxed whitespace-pre-wrap break-all bg-black/20 rounded p-2">{{ status.wan.wifi_log }}</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="wanStep === 'done'" class="mt-2 text-xs text-green-400">
|
2026-06-29 15:11:48 +00:00
|
|
|
|
WAN configured. Router is connecting…
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step: scan prompt -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'scan'">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="glass-button glass-button-warning w-full text-sm font-medium"
|
|
|
|
|
|
@click="scanWifi"
|
|
|
|
|
|
>
|
|
|
|
|
|
Scan for Networks
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<p v-if="wanError" class="mt-2 text-xs text-red-400">{{ wanError }}</p>
|
|
|
|
|
|
<button class="mt-3 text-xs text-white/40 hover:text-white" @click="wanStep = 'idle'">Cancel</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step: scanning -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'scanning'">
|
|
|
|
|
|
<div class="flex items-center gap-3 py-2">
|
|
|
|
|
|
<svg class="animate-spin w-4 h-4 text-white/50" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span class="text-sm text-white/50">Scanning for networks…</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step: network list -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'list'">
|
|
|
|
|
|
<p class="text-xs text-white/40 mb-3">{{ scannedNetworks.length }} network{{ scannedNetworks.length !== 1 ? 's' : '' }} found. Select one:</p>
|
|
|
|
|
|
<div v-if="scannedNetworks.length === 0" class="text-sm text-white/50">No networks found. Try scanning again.</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="net in scannedNetworks"
|
|
|
|
|
|
:key="net.bssid"
|
|
|
|
|
|
class="w-full flex items-center justify-between px-3 py-2.5 rounded-lg border border-white/10 hover:border-white/30 hover:bg-white/5 transition-colors text-left"
|
|
|
|
|
|
@click="selectNetwork(net)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2.5">
|
|
|
|
|
|
<!-- Signal bars -->
|
|
|
|
|
|
<div class="flex items-end gap-0.5 h-4">
|
|
|
|
|
|
<div v-for="b in 4" :key="b"
|
|
|
|
|
|
class="w-1 rounded-sm"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
b * 1 <= signalBars(net.signal) ? 'bg-white/70' : 'bg-white/15',
|
|
|
|
|
|
b === 1 ? 'h-1' : b === 2 ? 'h-2' : b === 3 ? 'h-3' : 'h-4'
|
|
|
|
|
|
]"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-white text-sm">{{ net.ssid || '(hidden)' }}</span>
|
|
|
|
|
|
<span v-if="net.encryption !== 'none'" class="text-white/30 text-xs">🔒</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-xs text-white/30 font-mono flex-shrink-0">ch{{ net.channel }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="mt-3 text-xs text-white/40 hover:text-white" @click="wanStep = 'scan'">← Scan again</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step: password entry -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'password'">
|
|
|
|
|
|
<p class="text-sm text-white mb-3">
|
|
|
|
|
|
Connect to <span class="font-semibold">{{ selectedNetwork?.ssid }}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-if="selectedNetwork?.encryption !== 'none'"
|
|
|
|
|
|
v-model="wanPassword"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="WiFi password"
|
|
|
|
|
|
class="w-full px-4 py-3 mb-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors"
|
2026-06-30 17:12:50 +00:00
|
|
|
|
@keyup.enter="wanStep = 'dhcp'"
|
2026-06-29 15:11:48 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="glass-button glass-button-success flex-1 text-sm font-medium"
|
|
|
|
|
|
:disabled="selectedNetwork?.encryption !== 'none' && !wanPassword"
|
|
|
|
|
|
:class="selectedNetwork?.encryption !== 'none' && !wanPassword ? 'opacity-40 cursor-not-allowed' : ''"
|
2026-06-30 17:12:50 +00:00
|
|
|
|
@click="wanStep = 'dhcp'"
|
2026-06-29 15:11:48 +00:00
|
|
|
|
>
|
2026-06-30 17:12:50 +00:00
|
|
|
|
Next →
|
2026-06-29 15:11:48 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
<button class="text-xs text-white/40 hover:text-white px-3 py-2" @click="wanStep = 'list'">←</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p v-if="wanError" class="mt-2 text-xs text-red-400">{{ wanError }}</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-30 17:12:50 +00:00
|
|
|
|
<!-- Step: DHCP + masquerade config -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'dhcp'">
|
|
|
|
|
|
<p class="text-xs text-white/50 mb-4">
|
|
|
|
|
|
Configure how this router hands out addresses to WiFi clients.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- LAN info -->
|
|
|
|
|
|
<div class="mb-4 text-xs font-mono bg-black/20 rounded-lg px-3 py-2 text-white/50">
|
|
|
|
|
|
<span class="text-white/30">LAN: </span>{{ status?.wan?.lan_ip || '192.168.1.1' }}/24
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- DHCP range -->
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
|
<label class="block text-xs text-white/40 mb-2">DHCP range for clients</label>
|
|
|
|
|
|
<div class="flex items-center gap-2 text-sm">
|
|
|
|
|
|
<span class="text-white/40 font-mono text-xs">{{ status?.wan?.lan_ip?.split('.').slice(0,3).join('.') || '192.168.1' }}.</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="dhcpStart"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="2" max="250"
|
|
|
|
|
|
class="w-20 px-2 py-1.5 bg-transparent border border-white/20 rounded text-white text-sm text-center focus:outline-none focus:border-white/40"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-white/30">–</span>
|
|
|
|
|
|
<span class="text-white/60 font-mono text-xs">{{ Math.min(254, dhcpStart + dhcpLimit - 1) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="text-xs text-white/30 mt-1">{{ dhcpLimit }} addresses</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Masquerade -->
|
|
|
|
|
|
<div class="flex items-center justify-between mb-4 py-3 border-t border-white/10">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-sm text-white">Enable NAT masquerade</div>
|
|
|
|
|
|
<div class="text-xs text-white/40">Routes LAN traffic through the WiFi uplink</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="relative w-11 h-6 rounded-full transition-colors flex-shrink-0"
|
|
|
|
|
|
:class="masqEnabled ? 'bg-green-500/60' : 'bg-white/15'"
|
|
|
|
|
|
@click="masqEnabled = !masqEnabled"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform"
|
|
|
|
|
|
:class="masqEnabled ? 'translate-x-5' : 'translate-x-0.5'"
|
|
|
|
|
|
></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="wanError" class="mb-3 text-xs text-red-400">{{ wanError }}</p>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="glass-button glass-button-success flex-1 text-sm font-medium"
|
|
|
|
|
|
@click="configureWan"
|
|
|
|
|
|
>
|
|
|
|
|
|
Apply Settings
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="text-xs text-white/40 hover:text-white px-3 py-2" @click="wanStep = 'password'">←</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<!-- Step: applying -->
|
|
|
|
|
|
<template v-else-if="wanStep === 'connecting'">
|
|
|
|
|
|
<div class="flex items-center gap-3 py-2">
|
|
|
|
|
|
<svg class="animate-spin w-4 h-4 text-white/50" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span class="text-sm text-white/50">Applying WAN config…</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<!-- TollGate -->
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div class="glass-card p-6 mb-4">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<h2 class="text-sm font-semibold text-white/80 mb-4">TollGate</h2>
|
|
|
|
|
|
|
2026-06-29 01:22:45 +00:00
|
|
|
|
<div v-if="!status.tollgate.installed">
|
|
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-white/20 inline-block"></span>
|
|
|
|
|
|
<span class="text-sm text-white/50">Not installed</span>
|
|
|
|
|
|
</div>
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<div v-if="!status.wan?.internet" class="mb-3 text-xs text-yellow-400/80 bg-yellow-400/10 rounded-lg px-3 py-2">
|
|
|
|
|
|
Router needs internet access to install TollGate. Configure WAN above first.
|
|
|
|
|
|
</div>
|
2026-06-29 01:22:45 +00:00
|
|
|
|
<button
|
|
|
|
|
|
:disabled="provisioning"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
class="glass-button glass-button-success w-full text-sm font-medium"
|
|
|
|
|
|
:class="provisioning ? 'opacity-40 cursor-not-allowed' : ''"
|
2026-06-29 01:22:45 +00:00
|
|
|
|
@click="provisionTollgate"
|
|
|
|
|
|
>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
{{ provisioning ? 'Installing… this may take a few minutes' : 'Install TollGate' }}
|
2026-06-29 01:22:45 +00:00
|
|
|
|
</button>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<p v-if="provisionError" class="mt-3 text-xs text-red-400">{{ provisionError }}</p>
|
|
|
|
|
|
<p v-if="provisionSuccess && !provisioning" class="mt-3 text-xs text-green-400">TollGate provisioned successfully.</p>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-4">
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<span class="w-2 h-2 rounded-full inline-block"
|
|
|
|
|
|
:class="status.tollgate.enabled ? 'bg-green-400' : 'bg-yellow-400'"></span>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<span class="text-sm" :class="status.tollgate.enabled ? 'text-green-300' : 'text-yellow-300'">
|
|
|
|
|
|
{{ status.tollgate.enabled ? 'Enabled' : 'Disabled' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Price</dt>
|
|
|
|
|
|
<dd class="text-white font-medium">
|
|
|
|
|
|
{{ status.tollgate.price_per_step }} {{ status.tollgate.currency }}
|
|
|
|
|
|
/ {{ formatStepSize(status.tollgate.step_size_ms ?? 60000) }}
|
|
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Metric</dt>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<dd class="text-white/70 capitalize">{{ status.tollgate.metric }}</dd>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-span-2">
|
|
|
|
|
|
<dt class="text-xs text-white/40 mb-0.5">Mint URL</dt>
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<dd class="text-white/70 font-mono text-xs break-all">{{ status.tollgate.mint_url }}</dd>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- WiFi interfaces -->
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div class="glass-card p-6">
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<h2 class="text-sm font-semibold text-white/80 mb-4">
|
|
|
|
|
|
WiFi Interfaces
|
|
|
|
|
|
<span class="ml-2 text-xs font-normal text-white/40">(AP mode)</span>
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
2026-06-29 01:27:01 +00:00
|
|
|
|
<div v-if="status.wifi_interfaces.length === 0" class="text-sm text-white/50">
|
|
|
|
|
|
No AP interfaces configured.
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="iface in status.wifi_interfaces"
|
|
|
|
|
|
:key="iface.section"
|
2026-06-29 01:27:01 +00:00
|
|
|
|
class="flex items-start justify-between py-3 border-b border-white/10 last:border-0"
|
2026-06-28 19:28:25 +00:00
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-0.5">
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full inline-block"
|
|
|
|
|
|
:class="iface.disabled ? 'bg-white/20' : 'bg-green-400'"></span>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
<span class="text-sm font-medium text-white">{{ iface.ssid || '(hidden)' }}</span>
|
2026-06-29 15:11:48 +00:00
|
|
|
|
<span v-if="iface.network === 'tollgate'"
|
|
|
|
|
|
class="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-300">TollGate</span>
|
2026-06-28 19:28:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-white/40 ml-3.5">
|
|
|
|
|
|
{{ iface.device }} · {{ iface.encryption === 'none' ? 'Open' : iface.encryption }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-xs text-white/30 font-mono mt-0.5">{{ iface.section }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|