"use client"; import { AnimatePresence, motion } from "framer-motion"; import { Activity, ArrowUpRight, ChevronRight, CirclePlay, Clock, Cpu, HardDrive, Loader2, Menu, MonitorPlay, Network, RefreshCw, Server, Settings, Smartphone, Sparkles, Wifi, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import OverviewCharts from "@/components/OverviewCharts"; import { LiveAndroidWorkstation } from "@/components/live-product/LiveAndroidWorkstation"; import { LiveTiktokHdmiView } from "@/components/live-product/LiveTiktokHdmiView"; import { LiveYoutubeUnmannedView } from "@/components/live-product/LiveYoutubeUnmannedView"; import { normalizeBrowserReachableHttpUrl } from "@/lib/panelUrls"; type Lang = "zh" | "en"; type ViewKey = | "dashboard" | "services" | "live_youtube" | "live_tiktok" | "android" | "network" | "settings"; const apiBase = () => (typeof process !== "undefined" && process.env.NEXT_PUBLIC_API_URL) || ""; function tr(lang: Lang, zh: string, en: string) { return lang === "zh" ? zh : en; } type HubDashboard = { snapshot?: { cpu_load?: { "1m"?: number; "5m"?: number; "15m"?: number }; memory?: { total?: number; available?: number; free?: number }; disk?: { total?: number; free?: number; used?: number }; netdata?: { available?: boolean; version?: string }; uptime_seconds?: number; }; hardware?: Record; stack?: { hostname?: string; mdns_host?: string; machine?: string; kernel?: string; python?: string; platform?: string; ips?: string[]; quick_links?: Record; neko_login?: { user_password?: string; admin_password?: string; note_zh?: string }; neko_firefox_enabled?: boolean; }; services?: Array<{ service_id: string; label: string; description: string; available: boolean; running: boolean; status: string; detail?: string; url?: string; }>; android?: { adb_available: boolean; devices: Array<{ serial: string; model?: string; state: string }> }; live?: { backend_mode?: string; processes?: Array<{ pm2: string; label?: string; process_status?: string; script?: string; }>; }; network?: { interfaces?: Array<{ name?: string; rx_bytes?: number; tx_bytes?: number; rx_packets?: number; tx_packets?: number; }>; }; live_events?: { pm2_tail?: string[]; process_digest?: Array<{ pm2?: string; label?: string; status?: string }>; }; probes?: { srs?: { port?: number; api_port?: number; api_ok?: boolean; tcp?: { ok?: boolean; reachable?: boolean; latency_ms?: number; error?: string }; tcp_api?: { ok?: boolean; reachable?: boolean; latency_ms?: number; error?: string }; http?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string }; http_ui?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string }; }; neko?: { port?: number; tcp?: { ok?: boolean; reachable?: boolean; latency_ms?: number; error?: string }; http?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string }; }; neko_firefox?: { port?: number; tcp?: { ok?: boolean; reachable?: boolean; latency_ms?: number; error?: string }; http?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string }; }; }; }; type SrsProbe = NonNullable["srs"]; type ProcessEntry = { pm2: string; script: string; label: string }; function formatUptime(sec?: number) { if (sec == null || !Number.isFinite(sec)) return "—"; const s = Math.max(0, Math.floor(sec)); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); if (d > 0) return `${d}d ${h}h`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; } function formatBytes(value?: number) { if (!value) return "—"; const u = ["B", "KB", "MB", "GB", "TB"]; let n = value; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i += 1; } return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; } function statusPill(status: string) { if (status === "online" || status === "running") return "bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30"; if (status === "skipped") return "bg-slate-500/15 text-slate-300 ring-1 ring-slate-500/30"; if (status === "stopped" || status === "not_found") return "bg-zinc-500/15 text-zinc-400 ring-1 ring-zinc-500/25"; if (status === "error") return "bg-rose-500/15 text-rose-300 ring-1 ring-rose-500/35"; return "bg-violet-500/15 text-violet-200 ring-1 ring-violet-500/30"; } export default function LiveControlApp() { const base = apiBase(); const [lang, setLang] = useState("zh"); const [view, setView] = useState("dashboard"); const [mobileNav, setMobileNav] = useState(false); const [dash, setDash] = useState(null); const [dashErr, setDashErr] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [toast, setToast] = useState(null); const [entries, setEntries] = useState([]); const [currentProc, setCurrentProc] = useState(""); const [urlConfig, setUrlConfig] = useState(""); const [apkPath, setApkPath] = useState("/sdcard/app.apk"); const [androidSerial, setAndroidSerial] = useState(""); const [hubCfgSnapshot, setHubCfgSnapshot] = useState | null>(null); const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true); const [showStreamProbes, setShowStreamProbes] = useState(true); const t = useCallback((zh: string, en: string) => tr(lang, zh, en), [lang]); const notify = useCallback((msg: string) => { setToast(msg); window.setTimeout(() => setToast(null), 2800); }, []); const fetchJson = useCallback( async (path: string, init?: RequestInit): Promise => { const res = await fetch(`${base}${path}`, init); const data = (await res.json()) as T & { error?: string }; if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); return data; }, [base], ); const refreshDashboard = useCallback(async () => { try { const d = await fetchJson("/hub/dashboard"); setDash(d); setDashErr(null); const devs = d.android?.devices || []; if (devs.length && !androidSerial) setAndroidSerial(devs[0].serial); } catch (e) { setDashErr((e as Error).message); } finally { setLoading(false); } }, [androidSerial, fetchJson]); useEffect(() => { const s = window.localStorage.getItem("live-hub-lang"); if (s === "zh" || s === "en") setLang(s); }, []); useEffect(() => { window.localStorage.setItem("live-hub-lang", lang); }, [lang]); useEffect(() => { try { if (localStorage.getItem("livehub.autorefresh") === "0") setAutoRefreshEnabled(false); if (localStorage.getItem("livehub.showStreamProbes") === "0") setShowStreamProbes(false); } catch { /* ignore */ } }, []); useEffect(() => { void refreshDashboard(); if (!autoRefreshEnabled) return; const id = window.setInterval(() => void refreshDashboard(), 10000); return () => clearInterval(id); }, [refreshDashboard, autoRefreshEnabled]); useEffect(() => { void (async () => { try { const data = await fetchJson<{ entries?: ProcessEntry[] }>("/process_monitor"); const list = data.entries || []; setEntries(list); if (!currentProc && list[0]) setCurrentProc(list[0].pm2); } catch { /* ignore */ } })(); }, [currentProc, fetchJson]); useEffect(() => { if (!currentProc || (view !== "live_youtube" && view !== "live_tiktok")) return; void (async () => { try { const data = await fetchJson<{ content?: string }>( `/get_url_config?process=${encodeURIComponent(currentProc)}`, ); setUrlConfig(data.content || ""); } catch { setUrlConfig(""); } })(); }, [currentProc, fetchJson, view]); useEffect(() => { if (view !== "settings") return; void (async () => { try { const cfg = await fetchJson>("/hub/config"); setHubCfgSnapshot(cfg); } catch { setHubCfgSnapshot(null); } })(); }, [fetchJson, view]); const ql = useMemo(() => { const raw = dash?.stack?.quick_links || {}; const o: Record = {}; for (const [k, v] of Object.entries(raw)) { if (/^https?:\/\//i.test(v)) o[k] = normalizeBrowserReachableHttpUrl(v); else o[k] = v; } return o; }, [dash?.stack?.quick_links]); const scrcpyUrl = useMemo( () => (ql.ws_scrcpy ? normalizeBrowserReachableHttpUrl(ql.ws_scrcpy) : ""), [ql.ws_scrcpy], ); const runService = async (service: string, action: "start" | "stop" | "restart") => { setBusy(`svc:${service}`); try { const data = await fetchJson<{ message?: string; status?: string; detail?: string }>("/service_action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ service, action }), }); if (data.status === "error") { notify(data.detail || data.message || t("操作失败", "Action failed")); } else { notify(data.message || "OK"); } await refreshDashboard(); } catch (e) { notify((e as Error).message); } finally { setBusy(null); } }; const runProcess = async (action: "start" | "stop" | "restart", processOverride?: string) => { const proc = (processOverride ?? currentProc).trim(); if (!proc) return; setBusy(`proc:${action}`); try { const path = `/${action}`; const data = await fetchJson<{ output?: string; message?: string }>(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ process: proc }), }); notify(String(data.output || data.message || "OK")); await refreshDashboard(); } catch (e) { notify((e as Error).message); } finally { setBusy(null); } }; const saveUrlConfig = async (overrideContent?: string) => { if (!currentProc) return; const content = overrideContent ?? urlConfig; setBusy("save:url"); try { const params = new URLSearchParams({ process: currentProc }); await fetchJson(`/save_url_config?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); notify(t("URL 配置已保存", "URL config saved")); } catch (e) { notify((e as Error).message); throw e; } finally { setBusy(null); } }; const androidAction = async (action: string, payload: Record = {}) => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } setBusy(`adb:${action}`); try { const data = await fetchJson<{ message?: string }>("/android/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, serial: androidSerial, payload }), }); notify(data.message || "OK"); } catch (e) { notify((e as Error).message); } finally { setBusy(null); } }; const navItems: { id: ViewKey; icon: typeof Activity; labelZh: string; labelEn: string }[] = [ { id: "dashboard", icon: Activity, labelZh: "总览", labelEn: "Dashboard" }, { id: "services", icon: Server, labelZh: "服务", labelEn: "Services" }, { id: "live_youtube", icon: CirclePlay, labelZh: "YouTube 无人直播", labelEn: "YouTube unmanned" }, { id: "live_tiktok", icon: MonitorPlay, labelZh: "TikTok 无人直播", labelEn: "TikTok unmanned" }, { id: "android", icon: Smartphone, labelZh: "安卓", labelEn: "Android" }, { id: "network", icon: Network, labelZh: "网络", labelEn: "Network" }, { id: "settings", icon: Settings, labelZh: "设置", labelEn: "Settings" }, ]; const copyText = (text: string) => { void navigator.clipboard.writeText(text).then( () => notify(t("已复制", "Copied")), () => notify(t("复制失败", "Copy failed")), ); }; const onlineServices = dash?.services?.filter((s) => s.running && s.available).length ?? 0; const totalServices = dash?.services?.length ?? 0; return (
{toast ? ( {toast} ) : null}
{/* Desktop sidebar */} {/* Main */}

{t( navItems.find((n) => n.id === view)?.labelZh || "", navItems.find((n) => n.id === view)?.labelEn || "", )}

{dash?.stack?.mdns_host || "live.local"} · {t("暗黑", "Dark")} · {t("自动刷新", "Auto refresh")}

{view === "dashboard" ? (
{dashErr ? (
{t("无法加载仪表盘:", "Dashboard error: ")} {dashErr}
) : null}
{[ { icon: Cpu, label: t("CPU 1m", "CPU 1m"), value: dash?.snapshot?.cpu_load?.["1m"]?.toFixed(2) ?? "—", sub: t("负载均值", "Load avg"), }, { icon: Activity, label: t("内存", "Memory"), value: formatBytes(dash?.snapshot?.memory?.available), sub: `/ ${formatBytes(dash?.snapshot?.memory?.total)}`, }, { icon: HardDrive, label: t("磁盘可用", "Disk free"), value: formatBytes(dash?.snapshot?.disk?.free), sub: `/ ${formatBytes(dash?.snapshot?.disk?.total)}`, }, { icon: Server, label: t("服务在线", "Services up"), value: `${onlineServices}/${totalServices || "—"}`, sub: t("基础服务层", "Infra layer"), }, { icon: Clock, label: t("运行时间", "Uptime"), value: formatUptime(dash?.snapshot?.uptime_seconds), sub: t("自开机", "Since boot"), }, ].map((card, i) => (

{card.label}

{loading ? "…" : card.value}

{card.sub}

))}
{dash?.hardware && typeof dash.hardware === "object" ? (

{t("系统硬件与软件能力", "Hardware & software profile")}

{t( "来自 hardware_probe;用于直播/解码策略参考。", "From hardware_probe; guides decode/streaming posture.", )}

{t("平台", "Platform")}

arch
{String(dash.hardware.arch ?? "—")}
kernel
{String(dash.hardware.kernel ?? dash.stack?.kernel ?? "—")}
Python
{String(dash.stack?.python ?? "—")}
mDNS
{dash.stack?.mdns_host ?? "—"}
IP
{(dash.stack?.ips || []).join(" · ") || "—"}

GPU / NPU

{(() => { const gpu = dash.hardware.gpu as | { present?: boolean; driver?: string; cards?: string[]; render_nodes?: string[] } | undefined; const npu = dash.hardware.npu as { present?: boolean; devices?: string[] } | undefined; return (
GPU
{gpu?.present ? t("已检测", "present") : t("未检测", "absent")} {gpu?.driver ? ` · ${gpu.driver}` : ""}
render
{(gpu?.render_nodes || []).join(", ") || "—"}
NPU
{npu?.present ? (npu.devices || []).join(", ") : t("—", "—")}
); })()}

ffmpeg / 建议策略

{(() => { const acc = (dash.hardware.ffmpeg_hwaccels as string[] | undefined) || []; const rec = dash.hardware.recommendation as | { video_decoder?: string; hdmi_player?: string; browser_hwaccel?: string; stability_mode?: string; degradation_reasons?: string[]; } | undefined; return (

hwaccels: {acc.length ? acc.join(", ") : "—"}

{t("解码", "Decoder")}: {rec?.video_decoder ?? "—"}

HDMI: {rec?.hdmi_player ?? "—"}

Browser: {rec?.browser_hwaccel ?? "—"}

{t("稳定模式", "Stability")}: {rec?.stability_mode ?? "—"}

{(rec?.degradation_reasons || []).length ? (
    {(rec?.degradation_reasons || []).map((r) => (
  • {r}
  • ))}
) : null}
); })()}
) : null} {dash?.stack?.neko_login ? (

Neko {t("登录口令", "passwords")}

{dash.stack.neko_login.note_zh || t("Chromium :9200 与 Firefox :9201(若启用)共用下列口令。", "Chromium :9200 and Firefox :9201 share these.")}

{t("用户", "User")}

{dash.stack.neko_login.user_password ?? "—"}

{t("管理员", "Admin")}

{dash.stack.neko_login.admin_password ?? "—"}

{t( "Google 无官方 ARM 桌面 Chrome;Neko 使用 Chromium / Firefox 多架构镜像(非 Chrome 品牌)。", "No official ARM desktop Chrome; Neko uses Chromium/Firefox multi-arch images.", )}

) : null} {dash?.network?.interfaces?.length ? (

{t("网络流量(累计)", "Network I/O (cumulative)")}

{t("自开机以来各接口收发字节;非实时速率。", "Per-interface RX/TX since boot (not a live bitrate).")}

{(dash.network.interfaces || []).slice(0, 6).map((iface) => (

{iface.name}

↓ {formatBytes(iface.rx_bytes)} ·{" "} {iface.rx_packets?.toLocaleString() ?? "—"} pkt

↑ {formatBytes(iface.tx_bytes)} ·{" "} {iface.tx_packets?.toLocaleString() ?? "—"} pkt

))}
) : null} {(dash?.live_events?.pm2_tail?.length || dash?.live_events?.process_digest?.length) ? (

{t("直播 / PM2 事件", "Live / PM2 feed")}

{t("PM2 合并日志尾部", "PM2 merged log tail")}

                            {(dash?.live_events?.pm2_tail || []).join("\n") || t("暂无 PM2 输出", "No PM2 output")}
                          

{t("进程快照", "Process digest")}

    {(dash?.live_events?.process_digest || []).map((row) => (
  • {row.label || row.pm2} {row.status || "—"}
  • ))}
) : null} {dash?.probes && showStreamProbes ? (
{dash.probes.srs ? ( (() => { const sp = dash.probes?.srs as SrsProbe | undefined; const tcpOk = sp?.tcp?.reachable; const tcpApiOk = sp?.tcp_api?.reachable; const httpUi = sp?.http_ui; const http = sp?.http; const apiPort = sp?.api_port; const apiHealthy = sp?.api_ok ?? (http?.http_code === 200); const uiCode = httpUi?.http_code; return (

SRS

{apiHealthy ? t("API 正常", "API OK") : t("API 异常", "API down")}

:{sp?.port ?? "—"} / API :{apiPort ?? "—"} · TCP {tcpOk ? "OK" : "—"} · API TCP{" "} {tcpApiOk ? "OK" : "—"}

API HTTP {http?.http_code ?? "—"} · {t("根路径", "root")} {uiCode ?? "—"} {uiCode === 404 ? t("(无首页不影响推流)", " (no index is OK for streaming)") : null}

{t("延迟", "RTT")}{" "} {http?.latency_ms != null ? `${http.latency_ms} ms` : sp?.tcp?.latency_ms != null ? `${sp.tcp.latency_ms} ms` : "—"}

{!apiHealthy && http?.error ? (

{http.error}

) : null}
); })() ) : null} {dash.probes.neko ? ( (() => { const p = dash.probes?.neko; const tcpOk = p?.tcp?.reachable; const http = p?.http; return (

Neko Chromium

{t( "Docker 内 Chromium(非 Chrome 品牌);arm64/x86 同一镜像。", "Chromium in Docker (not Chrome branding); multi-arch.", )}

:{p?.port ?? "—"} · TCP {tcpOk ? "OK" : "—"} · HTTP {http?.http_code ?? "—"}

{t("延迟", "RTT")}{" "} {http?.latency_ms != null ? `${http.latency_ms} ms` : p?.tcp?.latency_ms != null ? `${p.tcp.latency_ms} ms` : "—"}

{!http?.reachable && http?.error ? (

{http.error}

) : null}
); })() ) : null} {dash.probes.neko_firefox ? ( (() => { const p = dash.probes?.neko_firefox; const tcpOk = p?.tcp?.reachable; const http = p?.http; return (

Neko Firefox

{t( "第二桌面(ENABLE_NEKO_FF=1);口令与 Chromium 相同。", "Second desktop (ENABLE_NEKO_FF=1); same passwords as Chromium.", )}

:{p?.port ?? "—"} · TCP {tcpOk ? "OK" : "—"} · HTTP {http?.http_code ?? "—"}

{t("延迟", "RTT")}{" "} {http?.latency_ms != null ? `${http.latency_ms} ms` : p?.tcp?.latency_ms != null ? `${p.tcp.latency_ms} ms` : "—"}

{!http?.reachable && http?.error ? (

{http.error}

) : null}
); })() ) : null}
) : null}

{t("直播进程", "Live processes")}

{dash?.live?.backend_mode || "—"}
    {(dash?.live?.processes || []).slice(0, 10).map((p) => (
  • LIVE

    {p.label || p.pm2}

    {p.script || p.pm2}

    {p.process_status || "—"}
  • ))} {!dash?.live?.processes?.length ? (
  • {t("暂无直播进程", "No live processes")}
  • ) : null}
) : null} {view === "services" ? (
{(dash?.services || []).map((s, i) => (

{s.label}

{s.description}

{s.running ? t("运行中", "Running") : s.status}
{s.detail ? (

{s.detail}

) : null} {s.service_id === "shellcrash" && s.available && !s.running ? (

{t( "stopped 为常态:仅当需要透明代理 / 规则分流时再点 Start(或 systemctl enable --now shellcrash)。", "Stopped is normal; Start only when you need the proxy (or enable shellcrash.service).", )}

) : null}
{s.url ? ( {t("打开面板", "Open panel")} ) : null}
))} {!dash?.services?.length ? (

{t("暂无服务数据", "No services")}

) : null}
) : null} {view === "live_youtube" ? ( void runProcess(a, pm)} onSaveUrlConfig={(c) => saveUrlConfig(c)} fetchJson={fetchJson} /> ) : null} {view === "live_tiktok" ? ( void runProcess(a)} onSaveUrlConfig={(c) => saveUrlConfig(c)} onCopy={copyText} fetchJson={fetchJson} /> ) : null} {view === "android" ? ( androidAction(action, payload ?? {})} onCopy={copyText} /> ) : null} {view === "network" ? (

{t("ShellCrash 网络面板", "ShellCrash panel")}

{t( "此页仅用于透明代理 / 规则分流。点击下方进入 ShellCrash 专属控制台;流媒体与其它服务请在「服务」页管理。", "Proxy rules only. Open ShellCrash below; use Services for SRS/Neko/etc.", )}

{[ { tx: t("互联网", "Internet"), c: "from-cyan-400 to-blue-500" }, { tx: "ShellCrash", c: "from-violet-500 to-fuchsia-500" }, { tx: t("本机 / 安卓", "Host / Android"), c: "from-emerald-400 to-teal-500" }, ].map((n, i) => (
{n.tx}
{i < 2 ? : null}
))}
{t("打开 ShellCrash 控制台", "Open ShellCrash console")}
) : null} {view === "settings" ? (

{t("常用选项(本机偏好);写入 JSON 配置请用高级控制台。", "Local preferences; use Advanced console for JSON.")}

{t("配置快照", "Config snapshot")}

{String((hubCfgSnapshot as { config_root?: string })?.config_root || "—")}

{t("主机名", "Hostname")}:{" "} {(() => { const sys = hubCfgSnapshot?.system as { identity?: { hostname_alias?: string } } | undefined; const a = sys?.identity?.hostname_alias?.trim(); if (a) return a; const h = dash?.stack?.mdns_host || ""; return h.replace(/\.local$/i, "") || "—"; })()}

{(hubCfgSnapshot?.services as Array<{ id?: string; enabled?: boolean }> | undefined)?.slice(0, 8).map((svc) => ( {svc.id} {svc.enabled ? "ON" : "off"} ))}
{t("高级控制台 · 文件与 JSON", "Advanced console · files / JSON")}
) : null}
{/* Mobile drawer */} {mobileNav ? ( setMobileNav(false)} > e.stopPropagation()} >
) : null}
); }