import { lazy, Suspense, useCallback, useEffect, useState, type FormEvent } from "react"; import { ArrowClockwise, AndroidLogo, CarProfile, CheckCircle, Cpu, Gauge, HardDrives, House, Gear, GitBranch, DownloadSimple, Power, SignOut, TerminalWindow, WarningCircle, WifiHigh } from "@phosphor-icons/react"; import { AdbPage } from "./AdbPage"; import { CarPage } from "./CarPage"; import { ApiError, getAuthState, getStatus, getNetworkStatus, getNetworkActivity, getUpdateStatus, post, type AuthState, type SystemStatus, type NetworkStatus, type NetworkActivity, type UpdateStatus } from "./api"; const ShellTerminal = lazy(async () => { const module = await import("./ShellTerminal"); return { default: module.ShellTerminal }; }); type Screen = "loading" | "setup" | "login" | "dashboard" | "fatal"; export function App() { const [screen, setScreen] = useState("loading"); const [auth, setAuth] = useState(null); const [fatalError, setFatalError] = useState(""); const refreshAuth = useCallback(async () => { try { const state = await getAuthState(); setAuth(state); setScreen(state.setupRequired ? "setup" : state.user ? "dashboard" : "login"); } catch (error) { setFatalError(error instanceof Error ? error.message : "The companion service is unreachable"); setScreen("fatal"); } }, []); useEffect(() => void refreshAuth(), [refreshAuth]); if (screen === "loading") return ; if (screen === "fatal") return void refreshAuth()} />; if (!auth) return null; if (screen === "setup") return ; if (screen === "login") return ; return ; } function BootScreen() { return (

Pi Car Companion

Connecting to your Pi

); } function ConnectionError({ message, retry }: { message: string; retry: () => void }) { return (
); } function AccountScreen({ mode, csrfToken, onComplete }: { mode: "setup" | "login"; csrfToken: string; onComplete: () => Promise; }) { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); const setup = mode === "setup"; async function submit(event: React.FormEvent) { event.preventDefault(); setError(""); setSubmitting(true); try { if (setup) await post("/api/auth/setup", { username, password }, csrfToken); await post("/api/auth/login", { username, password }, csrfToken); await onComplete(); } catch (caught) { setError(caught instanceof ApiError ? caught.message : "The request could not be completed"); } finally { setSubmitting(false); } } return (
Pi Car Companion

{setup ? "First run" : "Local access"}

{setup ? "Secure your companion." : "Welcome back."}

{setup ? "Create the only initial administrator. There are no default credentials." : "Sign in to view live Pi status and controlled actions."}

Credentials stay on this Pi.

void submit(event)}>

{setup ? "Create administrator" : "Sign in"}

{error &&
{error}
}
); } function Dashboard({ auth, onLoggedOut }: { auth: AuthState; onLoggedOut: () => Promise }) { const [page, setPage] = useState<"home" | "car" | "adb" | "network" | "settings">("home"); const [status, setStatus] = useState(null); const [error, setError] = useState(""); const [connection, setConnection] = useState<"connecting" | "live" | "offline">("connecting"); const [refreshing, setRefreshing] = useState(false); const refresh = useCallback(async () => { setRefreshing(true); try { setStatus(await getStatus()); setError(""); } catch (caught) { setError(caught instanceof Error ? caught.message : "Status could not be loaded"); } finally { setRefreshing(false); } }, []); useEffect(() => { void refresh(); const events = new EventSource("/api/events"); events.addEventListener("open", () => setConnection("live")); events.addEventListener("status", (event) => { setStatus(JSON.parse((event as MessageEvent).data) as SystemStatus); setConnection("live"); setError(""); }); events.addEventListener("error", () => setConnection("offline")); return () => events.close(); }, [refresh]); async function logout() { await post("/api/auth/logout", {}, auth.csrfToken); await onLoggedOut(); } return (

{page === "home" ? "System overview" : page === "car" ? "Live vehicle signals" : page === "adb" ? "Android device bridge" : page === "network" ? "Live diagnostics" : "Companion settings"}

{page === "home" ? status?.hostname ?? "Your Raspberry Pi" : page === "car" ? "Car" : page === "adb" ? "ADB" : page === "network" ? "Network activity" : "Settings"}

{page === "home" && }
{page === "home" ? <> {error &&
{error}
} {!status ? : } : page === "car" ? : page === "adb" ? : page === "network" ? : }
); } function NetworkActivityPage() { const [activity, setActivity] = useState(null); const [error, setError] = useState(""); const [paused, setPaused] = useState(false); const refresh = useCallback(async () => { try { setActivity(await getNetworkActivity()); setError(""); } catch (caught) { setError(caught instanceof Error ? caught.message : "Network activity is unavailable"); } }, []); useEffect(() => { void refresh(); if (paused) return; const interval = window.setInterval(() => void refresh(), 2_000); return () => window.clearInterval(interval); }, [paused, refresh]); const connected = activity?.clients.filter((client) => client.connected).length ?? 0; return (
Connected clients{connected}
Recent DNS queries{activity?.dnsQueries.length ?? 0}
Active flows{activity?.flows.length ?? 0}
Snapshot{activity?.supported ? new Date(activity.collectedAt).toLocaleTimeString() : "Unavailable"}
{error &&
{error}
} {activity?.messages.map((message) =>
{message}
)}

Hotspot clients

DHCP leases and current neighbor reachability.

{activity?.clients.length ? activity.clients.map((client) => (
)) : }

DNS requests

Query metadata from clients during the last 15 minutes.

{activity?.dnsAvailable ? "Live" : "Unavailable"}
{activity?.dnsQueries.map((query, index) => )}
TimeClientTypeName
{new Date(query.timestamp).toLocaleTimeString()}{query.clientAddress}{query.type}{query.name}
{!activity?.dnsQueries.length && }

Active connections

Destination metadata only; encrypted content remains private.

{activity?.flowsAvailable ? "Live" : "Unavailable"}
{activity?.flows.map((flow, index) => )}
ClientProtocolDestinationState
{flow.sourceAddress}{flow.protocol.toUpperCase()}{flow.destinationAddress}:{flow.destinationPort}{portLabel(flow.destinationPort)}{flow.state}
{!activity?.flows.length && }

This page records bounded connection metadata only. It does not capture payloads, passwords, cookies, or HTTPS paths.

); } function ActivityEmpty({ text }: { text: string }) { return
{text}
; } function portLabel(port: number): string { return ({ 53: "DNS", 80: "HTTP", 123: "NTP", 443: "HTTPS", 853: "DNS over TLS" } as Record)[port] ?? ""; } function SettingsPage({ csrfToken }: { csrfToken: string }) { const [tab, setTab] = useState<"network" | "system" | "shell">("network"); const [update, setUpdate] = useState(null); const [network, setNetwork] = useState(null); const [updateError, setUpdateError] = useState(""); const [networkError, setNetworkError] = useState(""); const [systemNotice, setSystemNotice] = useState(""); const [requesting, setRequesting] = useState(false); const [confirming, setConfirming] = useState(false); const [powerAction, setPowerAction] = useState<"reboot" | "poweroff" | null>(null); const [networkAction, setNetworkAction] = useState<"retry-upstream" | "restore-hotspot" | null>(null); const [hotspotSsid, setHotspotSsid] = useState("MG4-Companion"); const [hotspotPassword, setHotspotPassword] = useState(""); const [hotspotPasswordAgain, setHotspotPasswordAgain] = useState(""); const [ssidTouched, setSsidTouched] = useState(false); const [confirmHotspot, setConfirmHotspot] = useState(false); const [networkNotice, setNetworkNotice] = useState(""); const running = update?.state === "checking" || update?.state === "building"; const refreshUpdate = useCallback(async () => { try { setUpdate(await getUpdateStatus()); setUpdateError(""); } catch (caught) { setUpdateError(caught instanceof Error ? caught.message : "Update status is unavailable"); } }, []); const refreshNetwork = useCallback(async () => { try { setNetwork(await getNetworkStatus()); setNetworkError(""); } catch (caught) { setNetworkError(caught instanceof Error ? caught.message : "Network status is unavailable"); } }, []); useEffect(() => { void refreshUpdate(); void refreshNetwork(); const interval = window.setInterval(() => { void refreshUpdate(); void refreshNetwork(); }, 3_000); return () => window.clearInterval(interval); }, [refreshNetwork, refreshUpdate]); useEffect(() => { if (!ssidTouched && network?.hotspotSsid) setHotspotSsid(network.hotspotSsid); }, [network?.hotspotSsid, ssidTouched]); async function startUpdate() { setConfirming(false); setRequesting(true); setUpdateError(""); try { await post("/api/system/update", {}, csrfToken); await refreshUpdate(); } catch (caught) { setUpdateError(caught instanceof ApiError ? caught.message : "The update could not be started"); } finally { setRequesting(false); } } async function checkForUpdates() { setRequesting(true); setUpdateError(""); try { await post("/api/system/update/check", {}, csrfToken); window.setTimeout(() => void refreshUpdate(), 700); } catch (caught) { setUpdateError(caught instanceof ApiError ? caught.message : "The update check could not be started"); } finally { setRequesting(false); } } async function requestPowerAction() { if (!powerAction) return; const action = powerAction; setPowerAction(null); setRequesting(true); setUpdateError(""); try { await post("/api/system/power", { action }, csrfToken); setSystemNotice(action === "reboot" ? "Reboot scheduled. The dashboard will disconnect briefly." : "Power off scheduled. The Pi will shut down safely."); } catch (caught) { setUpdateError(caught instanceof ApiError ? caught.message : "The power action could not be scheduled"); } finally { setRequesting(false); } } async function requestNetworkAction() { if (!networkAction) return; const action = networkAction; setNetworkAction(null); setRequesting(true); setNetworkError(""); try { await post("/api/network/actions", { action }, csrfToken); await refreshNetwork(); } catch (caught) { setNetworkError(caught instanceof ApiError ? caught.message : "The network action could not be requested"); } finally { setRequesting(false); } } function prepareHotspotConfiguration(event: FormEvent) { event.preventDefault(); setNetworkError(""); setNetworkNotice(""); if (!/^[a-zA-Z0-9_. -]{1,32}$/.test(hotspotSsid.trim())) { setNetworkError("Use 1-32 letters, numbers, spaces, dots, hyphens, or underscores for the hotspot name"); return; } if (hotspotPassword.length < 8 || hotspotPassword.length > 63 || !/^[\x20-\x7e]+$/.test(hotspotPassword)) { setNetworkError("Use 8-63 printable characters for the hotspot password"); return; } if (hotspotPassword !== hotspotPasswordAgain) { setNetworkError("The hotspot passwords do not match"); return; } setConfirmHotspot(true); } async function configureHotspot() { setConfirmHotspot(false); setRequesting(true); setNetworkError(""); setNetworkNotice(""); try { await post("/api/network/configuration", { ssid: hotspotSsid.trim(), password: hotspotPassword }, csrfToken); setHotspotPassword(""); setHotspotPasswordAgain(""); setNetworkNotice("Hotspot configuration accepted. Reconnect using the new name and password if this page disconnects."); window.setTimeout(() => void refreshNetwork(), 2_000); } catch (caught) { setNetworkError(caught instanceof ApiError ? caught.message : "The hotspot could not be configured"); } finally { setRequesting(false); } } const revision = update?.installedRevision?.slice(0, 12) ?? "Unavailable"; const statusLabel = !update ? "Loading" : update.state === "current" ? "Up to date" : update.state === "available" ? "Update available" : update.state === "success" ? "Updated" : update.state === "failed" ? "Needs attention" : running ? (update.state === "checking" ? "Checking" : "Updating") : "Ready"; const modeLabels: Record["mode"], string> = { disabled: "Not configured", starting: "Starting", dual_radio: "Hotspot + internet", built_in_upstream: "Built-in upstream", probing_built_in: "Looking for internet", offline_hotspot: "Offline hotspot", degraded: "Needs attention", error: "Recovery failed" }; const probeSeconds = network?.probeDeadline ? Math.max(0, Math.ceil((Date.parse(network.probeDeadline) - Date.now()) / 1000)) : null; return (
{tab === "network" &&

Network

The built-in radio hosts the car network while the AWUS1900 provides upstream internet whenever it is available.

Current mode {network ? modeLabels[network.mode] : "Loading"}

{network?.message ?? "Reading network controller status..."}

{probeSeconds !== null && Offline hotspot recovery in at most {probeSeconds} seconds}
Hotspot {network?.hotspotSsid ?? (network ? "Not active" : "Loading")} {network?.hotspot.address ?? network?.hotspot.interface ?? "Waiting for status"}
Internet check {network?.connectivity ?? "unknown"} Reported by NetworkManager
{networkError &&
{networkError}
} {networkNotice &&
{networkNotice}
} {network?.lastFailure &&

Last issue: {network.lastFailure}

}

Hotspot credentials

Changing these settings may disconnect devices currently using the car hotspot.

{network?.hotspotSsid && Current: {network.hotspotSsid}}

The password is sent only to the local Pi configuration service and is stored by NetworkManager as a derived WPA key.

} {tab === "system" &&

General system

Restart or safely shut down the Raspberry Pi.

{systemNotice &&
{systemNotice}
}

Both actions use systemd for a clean shutdown. Power can be removed only after the Pi has stopped.

Software update

Check the configured Git branch, then verify and activate a newer release when you choose.

Status
{statusLabel}
Installed revision
{revision}
Available revision
{update?.toRevision?.slice(0, 12) ?? "Not checked"}
Last check
{update && update.updatedAt !== new Date(0).toISOString() ? new Date(update.updatedAt).toLocaleString() : "Never"}
{running && } {!running && update?.state === "failed" && } {!running && update?.state !== "failed" && } {update?.message ?? "Reading update status..."}
{updateError &&
{updateError}
}

The current release stays active unless the new version passes installation, lint, type checks, tests, and production builds.

Service behavior

Start on boot
{update?.supported ? "Enabled by systemd" : "Available after install"}
Failure recovery
{update?.supported ? "Automatic restart" : "Available after install"}
Update channel
{update?.supported ? "Configured Git branch" : "Not configured"}
} {tab === "shell" &&
Loading terminal…
}>
} {confirming &&
setConfirming(false)}>
event.stopPropagation()}>

Install the latest version?

The dashboard may disconnect briefly when the service restarts. The previous version remains active if verification fails.

} {networkAction &&
setNetworkAction(null)}>
event.stopPropagation()}>

{networkAction === "retry-upstream" ? "Try upstream networking?" : "Restore the car hotspot?"}

{networkAction === "retry-upstream" ? "If the AWUS1900 cannot connect, the built-in hotspot will disappear for up to 60 seconds while saved upstream networks are tried. It will be restored automatically after failure." : "The built-in adapter will leave its upstream network and restore the car hotspot. This dashboard connection may briefly disconnect."}

} {confirmHotspot &&
setConfirmHotspot(false)}>
event.stopPropagation()}>

Apply new hotspot credentials?

The hotspot will become {hotspotSsid.trim()}. Connected devices may disconnect and must reconnect with the new password.

} {powerAction &&
setPowerAction(null)}>
event.stopPropagation()}>

{powerAction === "reboot" ? "Reboot the Pi?" : "Power off the Pi?"}

{powerAction === "reboot" ? "The dashboard and hotspot will disconnect while the Pi restarts, then return automatically." : "The dashboard and hotspot will disconnect. Physical access may be required to turn the Pi back on."}

} ); } function NetworkAdapter({ label, adapter }: { label: string; adapter: NetworkStatus["hotspot"] | null }) { return (
{label} {!adapter ? "Loading" : !adapter.available ? "Unavailable" : adapter.connected ? adapter.connection ?? "Connected" : "Disconnected"} {adapter?.address ?? adapter?.interface ?? "Waiting for status"}
); } function StatusContent({ status }: { status: SystemStatus }) { const memoryPercent = percent(status.memory.usedBytes, status.memory.totalBytes); const diskPercent = status.disk.available ? percent(status.disk.value.usedBytes, status.disk.value.totalBytes) : null; const primaryAddress = status.network.interfaces[0]?.address; const temperature = status.cpu.temperatureCelsius.available ? `${status.cpu.temperatureCelsius.value.toFixed(1)}°C` : "Unavailable"; const headUnitLabel = status.headUnit.available ? status.headUnit.value.identity.model.available ? status.headUnit.value.identity.model.value : "Connected" : status.headUnit.state === "unauthorized" ? "Authorization required" : status.headUnit.state === "ambiguous" ? "Multiple devices" : status.headUnit.state === "disabled" ? "Not configured" : status.headUnit.state === "offline" ? "Offline" : "Unavailable"; return (

Companion service

Running normally

Host uptime
{formatUptime(status.uptimeSeconds)}
Service uptime
{formatUptime(status.service.processUptimeSeconds)}
Last update
{new Date(status.collectedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
} label="CPU temperature" value={temperature} note={`Load ${status.cpu.loadAverage[0]?.toFixed(2) ?? "Unavailable"}`} /> } label="Memory used" value={`${memoryPercent}%`} note={`${formatBytes(status.memory.availableBytes)} available`} meter={memoryPercent} /> } label="Disk used" value={diskPercent === null ? "Unavailable" : `${diskPercent}%`} note={status.disk.available ? `${formatBytes(status.disk.value.availableBytes)} available` : status.disk.reason} meter={diskPercent ?? undefined} /> } label="Local network" value={primaryAddress ?? "Unavailable"} note={status.network.interfaces[0]?.name ?? status.network.interfaceReason ?? "No active interface"} />
Operating system{status.operatingSystem}
Processor{status.cpu.model}
Wi-Fi detail{status.network.wifi.available ? status.network.wifi.value : status.network.wifi.reason}
ADB connection{headUnitLabel}
); } function Metric({ icon, label, value, note, meter }: { icon: React.ReactNode; label: string; value: string; note: string; meter?: number | undefined }) { return (
{icon}{label}
{value}

{note}

{meter !== undefined &&
}
); } function DashboardSkeleton() { return
; } function percent(used: number, total: number): number { return total > 0 ? Math.round((used / total) * 100) : 0; } function formatBytes(bytes: number): string { const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value.toFixed(unit < 2 ? 0 : 1)} ${units[unit]}`; } function formatUptime(seconds: number): string { const days = Math.floor(seconds / 86_400); const hours = Math.floor((seconds % 86_400) / 3_600); const minutes = Math.floor((seconds % 3_600) / 60); return days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; }