"use client"; import { AnimatePresence, motion } from "framer-motion"; import { Activity, ArrowUpRight, ChevronRight, CirclePlay, Clock, Cpu, HardDrive, Languages, Loader2, Menu, MonitorPlay, Moon, Network, RefreshCw, Server, Settings, Smartphone, Sparkles, Sun, Wifi, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LiveStudioScene3D } from "@/components/LiveStudioScene3D"; import OverviewCharts from "@/components/OverviewCharts"; import { LiveConsoleWorkspace } from "@/components/console/LiveConsoleWorkspace"; import { LiveAndroidWorkstation } from "@/components/live-product/LiveAndroidWorkstation"; import { LiveTiktokHdmiView } from "@/components/live-product/LiveTiktokHdmiView"; import type { PortalNavTarget } from "@/components/live-product/LivePipelineStrip"; import { LiveYoutubeUnmannedView } from "@/components/live-product/LiveYoutubeUnmannedView"; import { fetchJsonResponse } from "@/lib/fetchJson"; import { extractNekoBrowserLinks, formatNekoPorts, type NekoBrowserLink } from "@/lib/nekoLinks"; import { normalizeBrowserReachableHttpUrl } from "@/lib/panelUrls"; type Lang = "zh" | "en"; type PortalTheme = "dark" | "light"; 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_instances?: Array<{ id?: number; label?: string; port?: number; url?: string; quick_link_key?: string; }>; neko_login?: { user_login?: string; user_password?: string; admin_login?: string; admin_password?: string; note_zh?: string; }; }; 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; transport?: 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; rx_bps?: number; tx_bps?: number; sample_interval_seconds?: number; speed_mbps?: number | null; operstate?: string; }>; total_rx_bytes?: number; total_tx_bytes?: number; total_rx_bps?: number; total_tx_bps?: number; sample_interval_seconds?: number; }; live_events?: { pm2_tail?: string[]; process_digest?: Array<{ pm2?: string; label?: string; status?: string }>; }; hdmi?: { status?: string; can_stream_hdmi?: boolean; preferred_device?: string; video_devices?: string[]; usb_capture_matches?: string[]; issues?: string[]; suggested_ffmpeg_cmd?: 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 }; instances?: Array<{ id?: number; 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 }; }>; running_count?: number; required_count?: number; }; }; }; type DeploymentReport = { ready?: boolean; pass_count?: number; total_count?: number; required_total?: number; mdns_host?: string; primary_lan_ip?: string; port?: string; live_edge_enabled?: boolean; mdns_enabled?: boolean; checks?: Array<{ id: string; label: string; ok: boolean; kind?: string; detail?: string; url?: string; target?: string; http_code?: number; reachable?: boolean; }>; failing_checks?: Array<{ id: string; label: string; ok: boolean; detail?: string }>; hints?: string[]; urls?: Record; }; type SrsProbe = NonNullable["srs"]; type ProcessEntry = { pm2: string; script: string; label: string }; type ProcessMonitorRow = ProcessEntry & { process_status?: string; business_status?: string; business_note?: string; is_pushing?: boolean; }; 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 == null || !Number.isFinite(value)) return "—"; if (value === 0) return "0 B"; 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 formatRate(value?: number) { const text = formatBytes(value); return text === "—" ? text : `${text}/s`; } 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 === "configured") return "bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/25"; 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"; } function statusLabel(status: string, t: (zh: string, en: string) => string) { const normalized = (status || "").trim().toLowerCase(); if (normalized === "online" || normalized === "running") return t("运行中", "Running"); if (normalized === "configured") return t("已配置", "Configured"); if (normalized === "not_found") return t("未注册", "Unregistered"); if (normalized === "stopped") return t("已停止", "Stopped"); if (normalized === "error") return t("异常", "Error"); return status || "—"; } export default function LiveControlApp() { const base = apiBase(); const [lang, setLang] = useState("zh"); const [portalTheme, setPortalTheme] = useState("dark"); 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 [lastSyncedAt, setLastSyncedAt] = useState(null); const [pageVisible, setPageVisible] = useState(true); const [entries, setEntries] = useState([]); const [processRows, setProcessRows] = 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 [deployCheck, setDeployCheck] = useState(null); const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true); const [showStreamProbes, setShowStreamProbes] = useState(true); const dashboardInFlightRef = useRef | null>(null); const processMonitorInFlightRef = useRef | null>(null); const currentProcRef = useRef(currentProc); const urlConfigRequestSeqRef = useRef(0); const toastTimerRef = useRef(null); const t = useCallback((zh: string, en: string) => tr(lang, zh, en), [lang]); const notify = useCallback((msg: string) => { setToast(msg); if (toastTimerRef.current != null) { window.clearTimeout(toastTimerRef.current); } toastTimerRef.current = window.setTimeout(() => { setToast(null); toastTimerRef.current = null; }, 2800); }, []); const fetchJson = useCallback( async (path: string, init?: RequestInit): Promise => { return fetchJsonResponse(`${base}${path}`, init); }, [base], ); const refreshDashboard = useCallback(async () => { if (dashboardInFlightRef.current) return dashboardInFlightRef.current; const task = (async () => { try { const [dashResult, deployResult] = await Promise.allSettled([ fetchJson("/hub/dashboard"), fetchJson("/deploy/check"), ] as const); if (dashResult.status !== "fulfilled") { throw dashResult.reason; } const d = dashResult.value; setDash(d); setDashErr(null); setLastSyncedAt(Date.now()); if (deployResult.status === "fulfilled") { setDeployCheck(deployResult.value); } else { setDeployCheck(null); } const devs = d.android?.devices || []; if (devs.length && !androidSerial) { const ready = devs.find((x) => (x.state || "").toLowerCase() === "device"); setAndroidSerial((ready ?? devs[0]).serial); } } catch (e) { setDashErr((e as Error).message); setDeployCheck(null); } finally { setLoading(false); dashboardInFlightRef.current = null; } })(); dashboardInFlightRef.current = task; return task; }, [androidSerial, fetchJson]); const processMonitorPath = useMemo(() => { const params = new URLSearchParams({ light: "1" }); if (view === "live_youtube") params.set("family", "youtube"); else if (view === "live_tiktok") params.set("family", "tiktok"); return `/process_monitor?${params.toString()}`; }, [view]); const refreshProcessMonitor = useCallback(async () => { if (processMonitorInFlightRef.current) return processMonitorInFlightRef.current; const task = (async () => { try { const data = await fetchJson<{ entries?: ProcessMonitorRow[] }>(processMonitorPath); const list = data.entries || []; setProcessRows(list); setEntries(list.map((row) => ({ pm2: row.pm2, script: row.script, label: row.label }))); setLastSyncedAt(Date.now()); setCurrentProc((prev) => { if (!list.length) return prev; if (prev && list.some((row) => row.pm2 === prev)) return prev; return list[0].pm2; }); } catch { /* ignore */ } finally { processMonitorInFlightRef.current = null; } })(); processMonitorInFlightRef.current = task; return task; }, [fetchJson, processMonitorPath]); useEffect(() => { const s = window.localStorage.getItem("live-hub-lang"); if (s === "zh" || s === "en") setLang(s); const th = window.localStorage.getItem("live-hub-theme"); if (th === "light" || th === "dark") setPortalTheme(th); }, []); useEffect(() => { window.localStorage.setItem("live-hub-lang", lang); }, [lang]); useEffect(() => { document.documentElement.setAttribute("data-theme", portalTheme); try { window.localStorage.setItem("live-hub-theme", portalTheme); } catch { /* ignore */ } }, [portalTheme]); useEffect(() => { const onVis = () => setPageVisible(document.visibilityState === "visible"); onVis(); document.addEventListener("visibilitychange", onVis); return () => document.removeEventListener("visibilitychange", onVis); }, []); useEffect(() => { if (!mobileNav) return; const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setMobileNav(false); }; window.addEventListener("keydown", onKeyDown); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKeyDown); document.body.style.overflow = prev; }; }, [mobileNav]); useEffect( () => () => { if (toastTimerRef.current != null) { window.clearTimeout(toastTimerRef.current); } }, [], ); useEffect(() => { try { if (localStorage.getItem("livehub.autorefresh") === "0") setAutoRefreshEnabled(false); if (localStorage.getItem("livehub.showStreamProbes") === "0") setShowStreamProbes(false); } catch { /* ignore */ } }, []); useEffect(() => { void refreshDashboard(); }, [refreshDashboard]); useEffect(() => { void refreshProcessMonitor(); }, [refreshProcessMonitor]); useEffect(() => { if (!autoRefreshEnabled || !pageVisible || view === "live_youtube" || view === "live_tiktok") return; const id = window.setInterval(() => { void refreshDashboard(); }, 10000); return () => clearInterval(id); }, [autoRefreshEnabled, pageVisible, refreshDashboard, view]); useEffect(() => { if (!autoRefreshEnabled || !pageVisible) return; const id = window.setInterval(() => { void refreshProcessMonitor(); }, view === "live_youtube" || view === "live_tiktok" ? 700 : 4000); return () => clearInterval(id); }, [autoRefreshEnabled, pageVisible, refreshProcessMonitor, view]); useEffect(() => { currentProcRef.current = currentProc; }, [currentProc]); useEffect(() => { if (!currentProc || view !== "live_youtube") return; void (async () => { const targetProc = currentProcRef.current.trim(); const requestId = ++urlConfigRequestSeqRef.current; try { const params = new URLSearchParams({ process: targetProc, _ts: String(Date.now()), }); const data = await fetchJson<{ content?: string }>(`/get_url_config?${params}`); if (requestId !== urlConfigRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return; setUrlConfig(data.content || ""); } catch { /* 保留当前草稿,避免临时读取失败把编辑器清空 */ } })(); }, [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 nekoLinks = useMemo(() => extractNekoBrowserLinks(ql), [ql]); const nekoPortsLabel = useMemo(() => formatNekoPorts(nekoLinks), [nekoLinks]); const runService = async ( service: string, action: "install" | "start" | "stop" | "restart" | "uninstall" | "upgrade", ) => { 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}:${proc}`); 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 refreshProcessMonitor(); if (view === "live_youtube" || view === "live_tiktok") { void refreshDashboard(); } else { await refreshDashboard(); } } catch (e) { notify((e as Error).message); } finally { setBusy(null); } }; const saveUrlConfig = async (overrideContent?: string, processOverride?: string) => { const proc = (processOverride ?? currentProc).trim(); if (!proc) return; const content = overrideContent ?? urlConfig; const requestId = ++urlConfigRequestSeqRef.current; setBusy("save:url"); try { const params = new URLSearchParams({ process: proc }); await fetchJson(`/save_url_config?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); if (requestId === urlConfigRequestSeqRef.current && proc === currentProcRef.current.trim()) { setUrlConfig(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) => { const write = async () => { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } const ta = document.createElement("textarea"); ta.value = text; ta.readOnly = true; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); }; void write().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; const lastSyncLabel = lastSyncedAt ? new Date(lastSyncedAt).toLocaleTimeString(lang === "zh" ? "zh-CN" : "en-US", { hour12: false }) : t("未同步", "Not synced"); const refreshStateLabel = pageVisible ? t("前台自动刷新", "Auto refresh") : t("后台已暂停刷新", "Refresh paused in background"); 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"} ·{" "} {portalTheme === "dark" ? t("夜间", "Dark mode") : t("日间", "Light mode")} · {refreshStateLabel} ·{" "} {t("最后同步", "Last sync")} {lastSyncLabel}

{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} {deployCheck ? (

{t("安装验收", "Deployment health")}

{t( "一键脚本完成后,本机 API、局域网入口、反向代理与 mDNS 的可用性检查。", "Post-install checks for loopback API, LAN access, reverse proxy, and mDNS.", )}

{deployCheck.ready ? t("可交付", "Ready") : t("需处理", "Needs attention")}

{t("验收摘要", "Acceptance summary")}

{deployCheck.pass_count ?? 0}/{deployCheck.total_count ?? 0} {t("已通过", "checks passed")}

{t("必需项", "Required")}: {deployCheck.required_total ?? 0} · mDNS{" "} {deployCheck.mdns_enabled ? t("已启用", "enabled") : t("已关闭", "disabled")} · edge{" "} {deployCheck.live_edge_enabled ? t("已启用", "enabled") : t("已关闭", "disabled")}

{Object.entries(deployCheck.urls || {}) .filter(([, url]) => !!url) .slice(0, 4) .map(([key, url]) => ( {key} ))}

{deployCheck.failing_checks?.length ? t("失败项", "Failing checks") : t("当前状态", "Current state")}

{deployCheck.failing_checks?.length ? (
    {deployCheck.failing_checks.slice(0, 4).map((item) => (
  • {item.label} {item.detail || "—"}
  • ))}
) : (

{t("必需检查已全部通过,可直接用浏览器面板交付。", "All required checks pass; browser control plane is ready.")}

)} {(deployCheck.hints || []).length ? (
    {(deployCheck.hints || []).slice(0, 3).map((hint) => (
  • {hint}
  • ))}
) : null}
) : null} {dash?.stack?.neko_login ? (

Neko {t("登录", "sign-in")}

{dash.stack.neko_login.note_zh || t( nekoPortsLabel ? `Neko Chromium ${nekoPortsLabel} 使用下列账号策略。` : "Neko Chromium 多实例使用下列账号策略。", nekoPortsLabel ? `Neko Chromium ${nekoPortsLabel} uses the policy below.` : "Neko Chromium instances use the policy below.", )}

{t( "重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678)。用错密码会像一直加载;勿把「用户名/密码」写在一行。", "Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.", )}

{nekoLinks.length ? ( ) : null}

{t("昵称示例", "Display Name Example")}

{dash.stack.neko_login.user_login ?? "live"}

{t("连接口令", "Access Password")}

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

{t("管理昵称示例", "Admin Display Name Example")}

{dash.stack.neko_login.admin_login ?? "admin"}

{t("管理口令", "Admin Password")}

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

{t( "x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;默认保留管理员会话、开发者工具和 Tampermonkey 持久化。本栈未启用 Firefox 版 Neko。", "x86 defaults to Neko’s Chrome image, ARM defaults to Chromium, and admin access, DevTools, plus persistent Tampermonkey stay enabled. This stack does not use Firefox Neko.", )}

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

{t("网络带宽 / 流量", "Network bandwidth / traffic")}

{t( "实时速率由总览自动刷新间隔计算;累计值来自系统网卡计数器。", "Live rates are calculated from dashboard refresh deltas; totals come from kernel counters.", )}

{t("实时下行", "Live down")}

{formatRate(dash.network.total_rx_bps)}

{t("累计", "Total")}: {formatBytes(dash.network.total_rx_bytes)}

{t("实时上行", "Live up")}

{formatRate(dash.network.total_tx_bps)}

{t("累计", "Total")}: {formatBytes(dash.network.total_tx_bytes)}

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

{iface.name}

{t("链路", "Link")}: {iface.operstate || "unknown"} ·{" "} {iface.speed_mbps ? `${iface.speed_mbps} Mbps` : t("未知速率", "unknown speed")}

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

↑ {formatRate(iface.tx_bps)} ·{" "} {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} {statusLabel(row.status || "", t)}
  • ))}
) : 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}

    {statusLabel(p.process_status || "", t)}
  • ))} {!dash?.live?.processes?.length ? (
  • {t("暂无直播进程", "No live processes")}
  • ) : 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; const instances = p?.instances || []; const runningCount = p?.running_count ?? instances.filter((item) => item.tcp?.reachable).length; const requiredCount = p?.required_count ?? instances.length; return (

Neko Chromium

0 ? "bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-500/30" : "bg-rose-500/15 text-rose-200 ring-1 ring-rose-500/25" }`} > {runningCount}/{requiredCount || "—"} {t("在线", "online")}

{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` : "—"}

{instances.length ? (
{instances.map((item) => (

Neko {item.id ?? "?"}

:{item.port ?? "—"} · TCP {item.tcp?.reachable ? "OK" : "—"} · HTTP{" "} {item.http?.http_code ?? "—"}

{item.tcp?.reachable ? t("运行中", "Running") : t("未连通", "Offline")}
))}
) : null} {!http?.reachable && http?.error ? (

{http.error}

) : null}
); })() ) : null}
) : 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, pm) => saveUrlConfig(c, pm)} fetchJson={fetchJson} liveProcesses={processRows.length ? processRows : (dash?.live?.processes ?? [])} notify={notify} quickLinks={ql} onNavigate={(target: PortalNavTarget) => setView(target)} /> ) : null} {view === "live_tiktok" ? ( void runProcess(a)} onSaveUrlConfig={(c) => saveUrlConfig(c)} onReplayGoLive={async (replayUrl, label) => { const safe = label.replace(/[,,|]/g, "_"); await saveUrlConfig(`原画,${replayUrl},主播: ${safe}`); await runProcess("start"); }} onCopy={copyText} fetchJson={fetchJson} apiPrefix={base} quickLinks={ql} hdmiStatus={dash?.hdmi} notify={notify} onNavigate={(target: PortalNavTarget) => setView(target)} /> ) : null} {view === "android" ? ( androidAction(action, payload ?? {})} onCopy={copyText} onRefreshDevices={() => void refreshDashboard()} /> ) : null} {view === "network" ? (

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

{t( "透明代理与规则分流直接在本页完成,无需跳转;流媒体与其它系统服务请在「服务」页管理。", "Proxy rules and YAML editing are embedded here. Use Services for SRS/Neko/Chromium, 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}
))}
) : null} {view === "settings" ? (

{t("常用选项(本机偏好);完整文件与 JSON 编辑见下方链接。", "Local preferences; full file/JSON editor via the link below.")}

{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)", "Full config center (files / JSON)")}
) : null}
{/* Mobile drawer */} {mobileNav ? ( setMobileNav(false)} > e.stopPropagation()} >
) : null}
); }