296 lines
13 KiB
TypeScript
296 lines
13 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { RefreshCw, Wifi, WifiOff, RotateCcw, Shield, Cpu, Battery, HardDrive, Monitor } from 'lucide-react'
|
|
import {
|
|
GetDevices, GetDeviceInfo, GetSecurityOverview, EnableWirelessAdb,
|
|
ConnectWirelessAdb, DisconnectWirelessAdb, Reboot
|
|
} from '../../lib/wails'
|
|
import { notify } from '../../lib/notify'
|
|
import type { Device, DeviceInfo } from '../../lib/types'
|
|
|
|
interface SecurityOverview {
|
|
root: string; selinux: string; verifiedBoot: string; bootloaderLocked: string
|
|
encryption: string; securityPatch: string; dmVerity: string; debuggable: string
|
|
secure: string; buildType: string; buildTags: string; adbEnabled: string; devOptions: string
|
|
}
|
|
|
|
export default function ViewDashboard() {
|
|
const [devices, setDevices] = useState<Device[]>([])
|
|
const [info, setInfo] = useState<DeviceInfo | null>(null)
|
|
const [sec, setSec] = useState<SecurityOverview | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [infoLoading, setInfoLoading] = useState(false)
|
|
const [wirelessIp, setWirelessIp] = useState('')
|
|
const [wirelessPort, setWirelessPort] = useState('5555')
|
|
|
|
const refreshDevices = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const devs = await GetDevices()
|
|
setDevices(devs || [])
|
|
} catch (e: any) {
|
|
notify.error(e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
const loadDeviceInfo = useCallback(async () => {
|
|
setInfoLoading(true)
|
|
setInfo(null)
|
|
setSec(null)
|
|
try {
|
|
const [i, s] = await Promise.all([GetDeviceInfo(), GetSecurityOverview().catch(() => null)])
|
|
setInfo(i)
|
|
setSec(s)
|
|
} catch (e: any) {
|
|
notify.error(e)
|
|
} finally {
|
|
setInfoLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
refreshDevices()
|
|
}, [refreshDevices])
|
|
|
|
const handleEnableWireless = async () => {
|
|
const id = notify.loading('Enabling wireless ADB...')
|
|
try {
|
|
const out = await EnableWirelessAdb(wirelessPort)
|
|
notify.dismiss(id)
|
|
notify.success(out || 'Wireless ADB enabled')
|
|
} catch (e: any) {
|
|
notify.dismiss(id)
|
|
notify.error(e)
|
|
}
|
|
}
|
|
|
|
const handleConnect = async () => {
|
|
if (!wirelessIp) { notify.error('Enter an IP address'); return }
|
|
const id = notify.loading(`Connecting to ${wirelessIp}:${wirelessPort}...`)
|
|
try {
|
|
const out = await ConnectWirelessAdb(wirelessIp, wirelessPort)
|
|
notify.dismiss(id)
|
|
notify.success(out)
|
|
refreshDevices()
|
|
} catch (e: any) {
|
|
notify.dismiss(id)
|
|
notify.error(e)
|
|
}
|
|
}
|
|
|
|
const handleDisconnect = async () => {
|
|
if (!wirelessIp) { notify.error('Enter an IP address'); return }
|
|
const id = notify.loading('Disconnecting...')
|
|
try {
|
|
const out = await DisconnectWirelessAdb(wirelessIp, wirelessPort)
|
|
notify.dismiss(id)
|
|
notify.success(out)
|
|
refreshDevices()
|
|
} catch (e: any) {
|
|
notify.dismiss(id)
|
|
notify.error(e)
|
|
}
|
|
}
|
|
|
|
const handleReboot = async (mode: string) => {
|
|
const label = mode || 'system'
|
|
const id = notify.loading(`Rebooting to ${label}...`)
|
|
try {
|
|
await Reboot(mode)
|
|
notify.dismiss(id)
|
|
notify.success(`Reboot to ${label} initiated`)
|
|
} catch (e: any) {
|
|
notify.dismiss(id)
|
|
notify.error(e)
|
|
}
|
|
}
|
|
|
|
const connectedDevices = devices.filter(d => d.status === 'device')
|
|
|
|
type Tone = 'good' | 'warn' | 'bad' | 'none'
|
|
const toneCls: Record<Tone, string> = {
|
|
good: 'text-accent-green', warn: 'text-warn', bad: 'text-danger', none: 'text-text-primary',
|
|
}
|
|
const secRows: { label: string; value: string; tone: Tone }[] = sec ? [
|
|
{ label: 'Bootloader', value: sec.bootloaderLocked, tone: sec.bootloaderLocked === 'Locked' ? 'good' : sec.bootloaderLocked === 'Unlocked' ? 'warn' : 'none' },
|
|
{ label: 'Root', value: sec.root, tone: sec.root.includes('su') ? 'warn' : 'good' },
|
|
{ label: 'SELinux', value: sec.selinux, tone: /enforc/i.test(sec.selinux) ? 'good' : /permiss/i.test(sec.selinux) ? 'bad' : 'none' },
|
|
{ label: 'Verified boot', value: sec.verifiedBoot, tone: sec.verifiedBoot === 'green' ? 'good' : (sec.verifiedBoot === 'orange' || sec.verifiedBoot === 'yellow') ? 'warn' : sec.verifiedBoot === 'red' ? 'bad' : 'none' },
|
|
{ label: 'dm-verity', value: sec.dmVerity, tone: /enforc/i.test(sec.dmVerity) ? 'good' : /disabled|logging/i.test(sec.dmVerity) ? 'warn' : 'none' },
|
|
{ label: 'Encryption', value: sec.encryption, tone: /^encrypted/i.test(sec.encryption) ? 'good' : /unencrypted/i.test(sec.encryption) ? 'bad' : 'none' },
|
|
{ label: 'Security patch', value: sec.securityPatch, tone: 'none' },
|
|
{ label: 'Build type', value: sec.buildType, tone: sec.buildType === 'user' ? 'good' : (sec.buildType === 'userdebug' || sec.buildType === 'eng') ? 'warn' : 'none' },
|
|
{ label: 'Build tags', value: sec.buildTags, tone: /release-keys/.test(sec.buildTags) ? 'good' : /test-keys/.test(sec.buildTags) ? 'warn' : 'none' },
|
|
{ label: 'ro.debuggable', value: sec.debuggable, tone: sec.debuggable === '1' ? 'bad' : sec.debuggable === '0' ? 'good' : 'none' },
|
|
{ label: 'ro.secure', value: sec.secure, tone: sec.secure === '0' ? 'bad' : sec.secure === '1' ? 'good' : 'none' },
|
|
{ label: 'ADB enabled', value: sec.adbEnabled, tone: sec.adbEnabled === '1' ? 'warn' : 'none' },
|
|
{ label: 'Dev options', value: sec.devOptions, tone: sec.devOptions === '1' ? 'warn' : 'none' },
|
|
] : []
|
|
|
|
return (
|
|
<div className="p-4 space-y-4 h-full overflow-auto">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-base font-medium text-text-primary">Dashboard</h1>
|
|
<button onClick={refreshDevices} disabled={loading} className="btn-ghost text-xs">
|
|
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
|
{/* Device List */}
|
|
<div className="card p-4 space-y-3">
|
|
<p className="section-title">Connected Devices</p>
|
|
{devices.length === 0 ? (
|
|
<div className="text-center py-6">
|
|
<p className="text-text-muted text-sm">No devices detected</p>
|
|
<p className="text-text-muted text-xs mt-1">Connect a device with USB debugging enabled</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{devices.map(d => (
|
|
<div key={d.serial} className="flex items-center justify-between bg-bg-raised rounded px-3 py-2">
|
|
<div>
|
|
<p className="mono text-text-primary text-xs">{d.serial}</p>
|
|
<p className="text-text-muted text-xs mt-0.5">{d.status}</p>
|
|
</div>
|
|
<span className={`status-dot ${d.status === 'device' ? 'status-dot-green' : 'status-dot-red'}`} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{connectedDevices.length > 0 && (
|
|
<button onClick={loadDeviceInfo} disabled={infoLoading} className="btn-ghost w-full text-xs justify-center">
|
|
{infoLoading ? <span className="animate-spin">↻</span> : null}
|
|
{infoLoading ? 'Loading...' : 'Load Device Info'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Device Info */}
|
|
<div className="card p-4 space-y-3 xl:col-span-2">
|
|
<p className="section-title">Device Information</p>
|
|
{!info && !infoLoading && (
|
|
<div className="text-center py-6">
|
|
<p className="text-text-muted text-sm">Select a device and click "Load Device Info"</p>
|
|
</div>
|
|
)}
|
|
{infoLoading && (
|
|
<div className="text-center py-6">
|
|
<div className="w-6 h-6 border-2 border-accent-green border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
|
<p className="text-text-muted text-sm">Fetching device details...</p>
|
|
</div>
|
|
)}
|
|
{info && (
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
|
{[
|
|
{ label: 'Model', value: `${info.brand} ${info.model}` },
|
|
{ label: 'Codename', value: info.codename },
|
|
{ label: 'Android', value: info.androidVersion },
|
|
{ label: 'Build', value: info.buildNumber },
|
|
{ label: 'Serial', value: info.serial, mono: true },
|
|
{ label: 'CPU Arch', value: info.cpuArch },
|
|
{ label: 'RAM', value: info.ramTotal, icon: <Cpu size={12} /> },
|
|
{ label: 'Storage', value: info.storageInfo, icon: <HardDrive size={12} /> },
|
|
{ label: 'Battery', value: info.batteryLevel, icon: <Battery size={12} /> },
|
|
{ label: 'Screen', value: info.screenResolution,icon: <Monitor size={12} /> },
|
|
{ label: 'IP Address', value: info.ipAddress },
|
|
{ label: 'Uptime', value: info.uptime },
|
|
{ label: 'Root', value: info.rootStatus, icon: <Shield size={12} /> },
|
|
{ label: 'Bootloader', value: info.bootloaderStatus },
|
|
{ label: 'Security Patch', value: info.securityPatch },
|
|
{ label: 'Kernel', value: info.kernelVersion, mono: true },
|
|
{ label: 'Baseband', value: info.basebandVersion, mono: true },
|
|
].map(({ label, value, mono, icon }) => (
|
|
<div key={label} className="flex items-start gap-2 min-w-0">
|
|
<span className="text-text-muted text-xs w-28 shrink-0 pt-0.5">{label}</span>
|
|
<span className={`text-xs text-text-primary truncate flex items-center gap-1 ${mono ? 'font-mono' : ''}`}>
|
|
{icon && <span className="text-text-muted">{icon}</span>}
|
|
{value || 'N/A'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Security Overview — quick audit */}
|
|
{sec && (
|
|
<div className="card p-4 space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Shield size={14} className="text-accent-green" />
|
|
<p className="section-title">Security Overview</p>
|
|
<span className="text-[11px] text-text-muted ml-1">quick device audit</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-x-6 gap-y-2">
|
|
{secRows.map(r => (
|
|
<div key={r.label} className="flex items-start gap-2 min-w-0">
|
|
<span className="text-text-muted text-xs w-24 shrink-0 pt-0.5">{r.label}</span>
|
|
<span className={`text-xs truncate ${toneCls[r.tone]}`}>{r.value || 'N/A'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
{/* Wireless ADB */}
|
|
<div className="card p-4 space-y-3">
|
|
<p className="section-title">Wireless ADB</p>
|
|
<div className="space-y-2">
|
|
<div className="flex gap-2">
|
|
<input
|
|
className="input"
|
|
placeholder="192.168.1.x"
|
|
value={wirelessIp}
|
|
onChange={e => setWirelessIp(e.target.value)}
|
|
/>
|
|
<input
|
|
className="input w-24 shrink-0"
|
|
placeholder="5555"
|
|
value={wirelessPort}
|
|
onChange={e => setWirelessPort(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={handleEnableWireless} className="btn-ghost flex-1 justify-center text-xs">
|
|
<Wifi size={13} /> Enable TCP/IP
|
|
</button>
|
|
<button onClick={handleConnect} className="btn-primary flex-1 justify-center text-xs">
|
|
<Wifi size={13} /> Connect
|
|
</button>
|
|
<button onClick={handleDisconnect} className="btn-ghost flex-1 justify-center text-xs">
|
|
<WifiOff size={13} /> Disconnect
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Reboot */}
|
|
<div className="card p-4 space-y-3">
|
|
<p className="section-title">Reboot Options</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{[
|
|
{ label: 'System', mode: '', cls: 'btn-ghost' },
|
|
{ label: 'Recovery', mode: 'recovery', cls: 'btn-warn' },
|
|
{ label: 'Bootloader', mode: 'bootloader', cls: 'btn-warn' },
|
|
{ label: 'Fastboot', mode: 'fastboot', cls: 'btn-ghost' },
|
|
].map(({ label, mode, cls }) => (
|
|
<button
|
|
key={label}
|
|
onClick={() => handleReboot(mode)}
|
|
className={`${cls} justify-center text-xs`}
|
|
>
|
|
<RotateCcw size={13} /> {label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|