s
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
Wifi,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { LiveStudioScene3D } from "@/components/LiveStudioScene3D";
|
||||
import OverviewCharts from "@/components/OverviewCharts";
|
||||
@@ -148,6 +148,12 @@ type HubDashboard = {
|
||||
type SrsProbe = NonNullable<HubDashboard["probes"]>["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 "—";
|
||||
@@ -208,6 +214,7 @@ export default function LiveControlApp() {
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
|
||||
const [entries, setEntries] = useState<ProcessEntry[]>([]);
|
||||
const [processRows, setProcessRows] = useState<ProcessMonitorRow[]>([]);
|
||||
const [currentProc, setCurrentProc] = useState("");
|
||||
const [urlConfig, setUrlConfig] = useState("");
|
||||
const [apkPath, setApkPath] = useState("/sdcard/app.apk");
|
||||
@@ -215,6 +222,8 @@ export default function LiveControlApp() {
|
||||
const [hubCfgSnapshot, setHubCfgSnapshot] = useState<Record<string, unknown> | null>(null);
|
||||
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true);
|
||||
const [showStreamProbes, setShowStreamProbes] = useState(true);
|
||||
const dashboardInFlightRef = useRef<Promise<void> | null>(null);
|
||||
const processMonitorInFlightRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
const t = useCallback((zh: string, en: string) => tr(lang, zh, en), [lang]);
|
||||
|
||||
@@ -231,22 +240,58 @@ export default function LiveControlApp() {
|
||||
);
|
||||
|
||||
const refreshDashboard = useCallback(async () => {
|
||||
try {
|
||||
const d = await fetchJson<HubDashboard>("/hub/dashboard");
|
||||
setDash(d);
|
||||
setDashErr(null);
|
||||
const devs = d.android?.devices || [];
|
||||
if (devs.length && !androidSerial) {
|
||||
const ready = devs.find((x) => (x.state || "").toLowerCase() === "device");
|
||||
setAndroidSerial((ready ?? devs[0]).serial);
|
||||
if (dashboardInFlightRef.current) return dashboardInFlightRef.current;
|
||||
const task = (async () => {
|
||||
try {
|
||||
const d = await fetchJson<HubDashboard>("/hub/dashboard");
|
||||
setDash(d);
|
||||
setDashErr(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);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
dashboardInFlightRef.current = null;
|
||||
}
|
||||
} catch (e) {
|
||||
setDashErr((e as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
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 })));
|
||||
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);
|
||||
@@ -278,23 +323,27 @@ export default function LiveControlApp() {
|
||||
|
||||
useEffect(() => {
|
||||
void refreshDashboard();
|
||||
if (!autoRefreshEnabled) return;
|
||||
const id = window.setInterval(() => void refreshDashboard(), 10000);
|
||||
return () => clearInterval(id);
|
||||
}, [refreshDashboard, autoRefreshEnabled]);
|
||||
}, [refreshDashboard]);
|
||||
|
||||
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]);
|
||||
void refreshProcessMonitor();
|
||||
}, [refreshProcessMonitor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefreshEnabled || view === "live_youtube" || view === "live_tiktok") return;
|
||||
const id = window.setInterval(() => {
|
||||
void refreshDashboard();
|
||||
}, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, [autoRefreshEnabled, refreshDashboard, view]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefreshEnabled) return;
|
||||
const id = window.setInterval(() => {
|
||||
void refreshProcessMonitor();
|
||||
}, view === "live_youtube" || view === "live_tiktok" ? 700 : 4000);
|
||||
return () => clearInterval(id);
|
||||
}, [autoRefreshEnabled, refreshProcessMonitor, view]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc || view !== "live_youtube") return;
|
||||
@@ -305,7 +354,7 @@ export default function LiveControlApp() {
|
||||
);
|
||||
setUrlConfig(data.content || "");
|
||||
} catch {
|
||||
setUrlConfig("");
|
||||
/* 保留当前草稿,避免临时读取失败把编辑器清空 */
|
||||
}
|
||||
})();
|
||||
}, [currentProc, fetchJson, view]);
|
||||
@@ -368,7 +417,12 @@ export default function LiveControlApp() {
|
||||
body: JSON.stringify({ process: proc }),
|
||||
});
|
||||
notify(String(data.output || data.message || "OK"));
|
||||
await refreshDashboard();
|
||||
await refreshProcessMonitor();
|
||||
if (view === "live_youtube" || view === "live_tiktok") {
|
||||
void refreshDashboard();
|
||||
} else {
|
||||
await refreshDashboard();
|
||||
}
|
||||
} catch (e) {
|
||||
notify((e as Error).message);
|
||||
} finally {
|
||||
@@ -376,12 +430,13 @@ export default function LiveControlApp() {
|
||||
}
|
||||
};
|
||||
|
||||
const saveUrlConfig = async (overrideContent?: string) => {
|
||||
if (!currentProc) return;
|
||||
const saveUrlConfig = async (overrideContent?: string, processOverride?: string) => {
|
||||
const proc = (processOverride ?? currentProc).trim();
|
||||
if (!proc) return;
|
||||
const content = overrideContent ?? urlConfig;
|
||||
setBusy("save:url");
|
||||
try {
|
||||
const params = new URLSearchParams({ process: currentProc });
|
||||
const params = new URLSearchParams({ process: proc });
|
||||
await fetchJson(`/save_url_config?${params}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -485,10 +540,10 @@ export default function LiveControlApp() {
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-[10px] font-semibold uppercase tracking-[0.2em] ${portalTheme === "light" ? "text-slate-500" : "text-zinc-500"}`}>
|
||||
Live Hub
|
||||
live.local
|
||||
</p>
|
||||
<p className={`text-sm font-semibold ${portalTheme === "light" ? "text-slate-900" : "text-white"}`}>
|
||||
{t("超级业务中心", "Control Center")}
|
||||
{t("无人直播系统", "Unmanned Live System")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -606,7 +661,7 @@ export default function LiveControlApp() {
|
||||
<button
|
||||
type="button"
|
||||
disabled={!!busy}
|
||||
onClick={() => void refreshDashboard()}
|
||||
onClick={() => void Promise.all([refreshDashboard(), refreshProcessMonitor()])}
|
||||
className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-medium transition disabled:opacity-40 ${
|
||||
portalTheme === "light"
|
||||
? "border-slate-200 bg-white text-slate-700 hover:bg-slate-50"
|
||||
@@ -866,8 +921,8 @@ export default function LiveControlApp() {
|
||||
</div>
|
||||
<p className="mt-3 text-[10px] text-zinc-600">
|
||||
{t(
|
||||
"x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;本栈未启用 Firefox 版 Neko。",
|
||||
"x86 defaults to Neko’s Chrome image, while ARM defaults to the Chromium image. This stack does not use the Firefox image.",
|
||||
"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.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1142,9 +1197,9 @@ export default function LiveControlApp() {
|
||||
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
|
||||
title={
|
||||
s.service_id === "android_gateway" && s.status === "skipped"
|
||||
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
|
||||
? t("单网口无需网关", "Not needed on single NIC")
|
||||
: !s.available
|
||||
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
|
||||
? s.detail || t("当前不可操作", "Unavailable")
|
||||
: undefined
|
||||
}
|
||||
onClick={() => void runService(s.service_id, "install")}
|
||||
@@ -1172,9 +1227,9 @@ export default function LiveControlApp() {
|
||||
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
|
||||
title={
|
||||
s.service_id === "android_gateway" && s.status === "skipped"
|
||||
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
|
||||
? t("单网口无需网关", "Not needed on single NIC")
|
||||
: !s.available
|
||||
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
|
||||
? s.detail || t("当前不可操作", "Unavailable")
|
||||
: undefined
|
||||
}
|
||||
onClick={() => void runService(s.service_id, "upgrade")}
|
||||
@@ -1202,9 +1257,9 @@ export default function LiveControlApp() {
|
||||
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
|
||||
title={
|
||||
s.service_id === "android_gateway" && s.status === "skipped"
|
||||
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
|
||||
? t("单网口无需网关", "Not needed on single NIC")
|
||||
: !s.available
|
||||
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
|
||||
? s.detail || t("当前不可操作", "Unavailable")
|
||||
: undefined
|
||||
}
|
||||
onClick={() => void runService(s.service_id, "uninstall")}
|
||||
@@ -1231,9 +1286,9 @@ export default function LiveControlApp() {
|
||||
urlConfig={urlConfig}
|
||||
setUrlConfig={setUrlConfig}
|
||||
onRunProcess={(a, pm) => void runProcess(a, pm)}
|
||||
onSaveUrlConfig={(c) => saveUrlConfig(c)}
|
||||
onSaveUrlConfig={(c, pm) => saveUrlConfig(c, pm)}
|
||||
fetchJson={fetchJson}
|
||||
liveProcesses={dash?.live?.processes ?? []}
|
||||
liveProcesses={processRows.length ? processRows : (dash?.live?.processes ?? [])}
|
||||
notify={notify}
|
||||
onNavigate={(target: PortalNavTarget) => setView(target)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user