feat(ui): auto-refresh overview/modules, quick config, HW re-probe, link-tile layout; deploy: npm ci+build, restart live-console
Made-with: Cursor
This commit is contained in:
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,13 +24,19 @@ REMOTE_BASE = os.environ.get("LIVE_DEPLOY_PATH", "/home/live/douyinyoutube").rst
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
FILES = [
|
||||
"src/control_plane.py",
|
||||
"src/web_process_backend.py",
|
||||
"web.py",
|
||||
"main.py",
|
||||
"scripts/hardware_probe.py",
|
||||
"douyin_youtube_ffplay.py",
|
||||
"web-console/package.json",
|
||||
"web-console/package-lock.json",
|
||||
"web-console/app/globals.css",
|
||||
"web-console/app/layout.tsx",
|
||||
"web-console/app/page.tsx",
|
||||
"web-console/middleware.ts",
|
||||
"web-console/components/OverviewCharts.tsx",
|
||||
]
|
||||
|
||||
LOG_DIR = REPO_ROOT / ".deploy_logs"
|
||||
@@ -182,7 +189,8 @@ def main() -> int:
|
||||
|
||||
py_compile = (
|
||||
f"cd {REMOTE_BASE} && python3 -m py_compile web.py "
|
||||
"src/web_process_backend.py scripts/hardware_probe.py douyin_youtube_ffplay.py main.py"
|
||||
"src/control_plane.py src/web_process_backend.py "
|
||||
"scripts/hardware_probe.py douyin_youtube_ffplay.py main.py"
|
||||
)
|
||||
code_py, _ = run_logged(client, log_path, "python_py_compile", py_compile, timeout=90)
|
||||
if code_py != 0:
|
||||
@@ -191,6 +199,7 @@ def main() -> int:
|
||||
|
||||
build_cmd = (
|
||||
f"cd {REMOTE_BASE}/web-console && "
|
||||
"(npm ci 2>/dev/null || npm install) && "
|
||||
"(command -v pnpm >/dev/null 2>&1 && pnpm run build || npm run build)"
|
||||
)
|
||||
code_b, _ = run_logged(client, log_path, "web_console_build", build_cmd, timeout=900)
|
||||
@@ -212,15 +221,23 @@ def main() -> int:
|
||||
"echo try_restart_done'"
|
||||
)
|
||||
run_logged(client, log_path, "sudo_try_restart_units", restart_cmd, timeout=60)
|
||||
|
||||
run_logged(
|
||||
client,
|
||||
log_path,
|
||||
"curl_health",
|
||||
"curl -sS -m 10 http://127.0.0.1:8001/health 2>&1 || "
|
||||
"curl -sS -m 10 http://127.0.0.1:8101/health 2>&1 || "
|
||||
"sudo_restart_live_console",
|
||||
f"echo '{PASS}' | sudo -S systemctl restart live-console.service",
|
||||
timeout=120,
|
||||
)
|
||||
_safe_print("等待服务监听…")
|
||||
time.sleep(6)
|
||||
run_logged(
|
||||
client,
|
||||
log_path,
|
||||
"curl_health_after_restart",
|
||||
"curl -sS -m 15 http://127.0.0.1:8001/health 2>&1 || "
|
||||
"curl -sS -m 15 http://127.0.0.1:8101/health 2>&1 || "
|
||||
"echo 'curl_health_failed'",
|
||||
timeout=20,
|
||||
timeout=25,
|
||||
)
|
||||
|
||||
run_logged(
|
||||
|
||||
@@ -64,6 +64,161 @@ html[data-theme="light"] .chart-surface {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .glass {
|
||||
@apply border-slate-200/90 shadow-slate-400/15;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-lg font-semibold tracking-tight;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
@apply mt-2 text-xs leading-relaxed;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
@apply rounded-2xl border px-4 py-3 text-xs font-mono leading-relaxed;
|
||||
border-color: rgb(255 255 255 / 0.1);
|
||||
background: rgb(0 0 0 / 0.22);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .meta-chip {
|
||||
border-color: rgb(226 232 240 / 0.95);
|
||||
background: rgb(255 255 255 / 0.72);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tab-strip {
|
||||
@apply -mx-1 flex gap-2 overflow-x-auto pb-1.5 pt-0.5 md:flex-wrap md:overflow-visible;
|
||||
scroll-snap-type: x proximity;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@apply btn-focus shrink-0 snap-start rounded-2xl border px-4 py-2.5 text-sm font-medium transition-colors duration-200;
|
||||
border-color: rgb(255 255 255 / 0.12);
|
||||
background: rgb(0 0 0 / 0.22);
|
||||
color: rgb(203 213 225);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: rgb(34 211 238 / 0.25);
|
||||
color: rgb(226 232 240);
|
||||
}
|
||||
|
||||
.tab-btn-active {
|
||||
border-color: rgb(34 211 238 / 0.45) !important;
|
||||
background: rgb(34 211 238 / 0.12) !important;
|
||||
color: rgb(207 250 254) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .tab-btn {
|
||||
border-color: rgb(226 232 240);
|
||||
background: rgb(255 255 255 / 0.75);
|
||||
color: rgb(71 85 105);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .tab-btn:hover {
|
||||
border-color: rgb(6 182 212 / 0.45);
|
||||
color: rgb(15 118 110);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .tab-btn-active {
|
||||
border-color: rgb(6 182 212 / 0.55) !important;
|
||||
background: rgb(207 250 254 / 0.55) !important;
|
||||
color: rgb(14 116 144) !important;
|
||||
}
|
||||
|
||||
.link-tile {
|
||||
@apply btn-focus block rounded-2xl border px-4 py-2.5 text-sm font-medium transition-colors duration-200;
|
||||
border-color: rgb(255 255 255 / 0.1);
|
||||
background: rgb(0 0 0 / 0.2);
|
||||
color: rgb(241 245 249);
|
||||
}
|
||||
|
||||
.link-tile:hover {
|
||||
border-color: rgb(34 211 238 / 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .link-tile {
|
||||
border-color: rgb(226 232 240);
|
||||
background: rgb(255 255 255 / 0.65);
|
||||
color: rgb(30 41 59);
|
||||
}
|
||||
|
||||
.link-tile-accent {
|
||||
border-color: rgb(34 211 238 / 0.28);
|
||||
background: rgb(34 211 238 / 0.1);
|
||||
color: rgb(236 254 255);
|
||||
}
|
||||
|
||||
.link-tile-accent:hover {
|
||||
border-color: rgb(34 211 238 / 0.55);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .link-tile-accent {
|
||||
border-color: rgb(6 182 212 / 0.35);
|
||||
background: rgb(207 250 254 / 0.65);
|
||||
color: rgb(14 116 144);
|
||||
}
|
||||
|
||||
.link-tile-warn {
|
||||
border-color: rgb(251 191 36 / 0.25);
|
||||
background: rgb(251 191 36 / 0.1);
|
||||
color: rgb(254 252 232);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .link-tile-warn {
|
||||
border-color: rgb(251 191 36 / 0.45);
|
||||
background: rgb(254 252 232 / 0.9);
|
||||
color: rgb(120 53 15);
|
||||
}
|
||||
|
||||
.service-card {
|
||||
@apply rounded-3xl border p-5 transition-shadow duration-200;
|
||||
border-color: rgb(255 255 255 / 0.08);
|
||||
background: rgb(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
box-shadow: 0 12px 40px -24px rgb(34 211 238 / 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .service-card {
|
||||
border-color: rgb(226 232 240 / 0.95);
|
||||
background: rgb(255 255 255 / 0.55);
|
||||
}
|
||||
|
||||
.panel-inset {
|
||||
@apply rounded-2xl border p-4 text-sm;
|
||||
border-color: rgb(255 255 255 / 0.08);
|
||||
background: rgb(0 0 0 / 0.2);
|
||||
color: rgb(203 213 225);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .panel-inset {
|
||||
border-color: rgb(226 232 240 / 0.85);
|
||||
background: rgb(255 255 255 / 0.5);
|
||||
color: rgb(51 65 85);
|
||||
}
|
||||
|
||||
.field-select {
|
||||
@apply w-full rounded-2xl border px-4 py-3 text-sm outline-none transition;
|
||||
border-color: rgb(255 255 255 / 0.12);
|
||||
background: rgb(2 6 23 / 0.55);
|
||||
color: rgb(248 250 252);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .field-select {
|
||||
border-color: rgb(226 232 240);
|
||||
background: rgb(255 255 255 / 0.9);
|
||||
color: rgb(15 23 42);
|
||||
}
|
||||
|
||||
.glow-text {
|
||||
|
||||
@@ -383,6 +383,29 @@ export default function Home() {
|
||||
[refreshFiles],
|
||||
);
|
||||
|
||||
const jumpToConfig = useCallback(
|
||||
(root: string, path: string) => {
|
||||
setTab("config");
|
||||
window.setTimeout(() => {
|
||||
void openManagedFile(root, path);
|
||||
}, 0);
|
||||
},
|
||||
[openManagedFile],
|
||||
);
|
||||
|
||||
const probeHardware = useCallback(async () => {
|
||||
setBusy("hardware:probe");
|
||||
try {
|
||||
const data = await fetchJson<HardwareProfile>("/hardware_profile?refresh=true");
|
||||
setHardware(data);
|
||||
notify(tr("硬件探测已更新", "Hardware profile refreshed"));
|
||||
} catch (reason) {
|
||||
notify((reason as Error).message);
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}, [fetchJson, notify, tr]);
|
||||
|
||||
const boot = useCallback(async () => {
|
||||
setBooting(true);
|
||||
setError(null);
|
||||
@@ -439,6 +462,14 @@ export default function Home() {
|
||||
return () => window.clearInterval(timer);
|
||||
}, [currentProcess, refreshProcess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (booting || error) return;
|
||||
if (tab !== "overview" && tab !== "modules") return;
|
||||
void refreshServices();
|
||||
const timer = window.setInterval(() => void refreshServices(), 10000);
|
||||
return () => clearInterval(timer);
|
||||
}, [booting, error, refreshServices, tab]);
|
||||
|
||||
useEffect(() => {
|
||||
const visibleRoots = tab === "shellcrash" ? shellRoots : tab === "config" ? otherRoots : roots;
|
||||
if (!visibleRoots.length) return;
|
||||
@@ -703,7 +734,7 @@ export default function Home() {
|
||||
<>
|
||||
<main className="safe-pb-nav mx-auto min-h-screen max-w-7xl px-4 pb-16 pt-8 sm:px-6 lg:px-10">
|
||||
{toast ? (
|
||||
<div className="animate-toast-in fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-2xl border border-cyan-500/25 bg-slate-950/95 px-5 py-3 text-sm text-cyan-50">
|
||||
<div className="animate-toast-in fixed bottom-24 left-1/2 z-50 max-w-[min(92vw,28rem)] -translate-x-1/2 rounded-2xl border border-cyan-500/25 bg-slate-950/95 px-5 py-3 text-center text-sm text-cyan-50 shadow-lg md:bottom-6 [html[data-theme=light]_&]:border-cyan-600/25 [html[data-theme=light]_&]:bg-white/95 [html[data-theme=light]_&]:text-cyan-950">
|
||||
{toast}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -725,11 +756,11 @@ export default function Home() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-xs text-slate-300">
|
||||
<div className="meta-chip max-w-full">
|
||||
{(stack?.mdns_host || "live.local") +
|
||||
" | " +
|
||||
" · " +
|
||||
(stack?.machine || "unknown") +
|
||||
" | " +
|
||||
" · " +
|
||||
(stack?.kernel || "unknown")}
|
||||
</div>
|
||||
<Button
|
||||
@@ -764,15 +795,12 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-wrap gap-2">
|
||||
<nav className="tab-strip" aria-label={tr("主导航", "Primary navigation")}>
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className={`btn-focus rounded-2xl px-4 py-2 text-sm ${
|
||||
tab === item
|
||||
? "border border-cyan-400/50 bg-cyan-500/10 text-cyan-100"
|
||||
: "border border-white/10 bg-black/20 text-slate-300"
|
||||
}`}
|
||||
type="button"
|
||||
className={`tab-btn ${tab === item ? "tab-btn-active" : ""}`}
|
||||
onClick={() => setTab(item)}
|
||||
>
|
||||
{{
|
||||
@@ -806,10 +834,8 @@ export default function Home() {
|
||||
<>
|
||||
<section className="grid gap-6 xl:grid-cols-12">
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-4">
|
||||
<h2 className="text-lg font-semibold text-[color:var(--text)]">
|
||||
{tr("🛰️ 流媒体与桌面入口", "Streaming & desktop")}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-slate-400 [html[data-theme=light]_&]:text-slate-600">
|
||||
<h2 className="section-title">{tr("🛰️ 流媒体与桌面入口", "Streaming & desktop")}</h2>
|
||||
<p className="section-lead">
|
||||
{tr(
|
||||
"ws-scrcpy:浏览器里控安卓;Chromium:本机+油猴+RemoteDebug;Neko:可选 Docker 远程桌面开 YouTube。",
|
||||
"ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222; Neko: optional Docker desktop.",
|
||||
@@ -817,110 +843,55 @@ export default function Home() {
|
||||
</p>
|
||||
<div className="mt-4 flex max-h-[28rem] flex-col gap-2 overflow-y-auto pr-1 log-scroll">
|
||||
{stack?.dashboard_url ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 transition hover:border-cyan-400/30 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={stack.dashboard_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={stack.dashboard_url} target="_blank" rel="noreferrer">
|
||||
🏠 Homepage
|
||||
</a>
|
||||
) : null}
|
||||
{stack?.control_url ? (
|
||||
<>
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 transition hover:border-cyan-400/30 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={`${stack.control_url}?tab=shellcrash&lang=${lang}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={`${stack.control_url}?tab=shellcrash&lang=${lang}`} target="_blank" rel="noreferrer">
|
||||
🛡️ ShellCrash
|
||||
</a>
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 transition hover:border-cyan-400/30 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={`${stack.control_url}?tab=android&lang=${lang}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={`${stack.control_url}?tab=android&lang=${lang}`} target="_blank" rel="noreferrer">
|
||||
🤖 {tr("安卓中心 (含 ws-scrcpy)", "Android hub + ws-scrcpy")}
|
||||
</a>
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 transition hover:border-cyan-400/30 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={`${stack.control_url}?tab=config&lang=${lang}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={`${stack.control_url}?tab=config&lang=${lang}`} target="_blank" rel="noreferrer">
|
||||
⚙️ {tr("配置中心", "Config")}
|
||||
</a>
|
||||
</>
|
||||
) : null}
|
||||
{ql.ws_scrcpy ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-cyan-500/25 bg-cyan-500/10 px-4 py-2.5 text-sm text-cyan-50 transition hover:border-cyan-400/50 [html[data-theme=light]_&]:border-cyan-300 [html[data-theme=light]_&]:bg-cyan-50 [html[data-theme=light]_&]:text-cyan-950"
|
||||
href={ql.ws_scrcpy}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile link-tile-accent" href={ql.ws_scrcpy} target="_blank" rel="noreferrer">
|
||||
📱 ws-scrcpy ({tr("原始流控制页", "raw stream UI")})
|
||||
</a>
|
||||
) : null}
|
||||
{ql.chromium_remote_debug ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 transition hover:border-violet-400/30 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={ql.chromium_remote_debug}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={ql.chromium_remote_debug} target="_blank" rel="noreferrer">
|
||||
🧪 {tr("Chromium RemoteDebug (9222)", "Chromium RemoteDebug")}
|
||||
</a>
|
||||
) : null}
|
||||
{ql.neko_browser ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-amber-500/20 bg-amber-500/10 px-4 py-2.5 text-sm text-amber-50 transition hover:border-amber-400/40 [html[data-theme=light]_&]:border-amber-200 [html[data-theme=light]_&]:bg-amber-50 [html[data-theme=light]_&]:text-amber-950"
|
||||
href={ql.neko_browser}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile link-tile-warn" href={ql.neko_browser} target="_blank" rel="noreferrer">
|
||||
🐱 Neko / {tr("Docker 浏览器桌面", "Docker browser desktop")}
|
||||
</a>
|
||||
) : null}
|
||||
{ql.webtty ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={ql.webtty}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={ql.webtty} target="_blank" rel="noreferrer">
|
||||
⌨️ WebTTY
|
||||
</a>
|
||||
) : null}
|
||||
{ql.filebrowser ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={ql.filebrowser}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={ql.filebrowser} target="_blank" rel="noreferrer">
|
||||
📂 File Browser
|
||||
</a>
|
||||
) : null}
|
||||
{ql.netdata ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={ql.netdata}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={ql.netdata} target="_blank" rel="noreferrer">
|
||||
📈 Netdata
|
||||
</a>
|
||||
) : null}
|
||||
{ql.srs_http ? (
|
||||
<a
|
||||
className="btn-focus rounded-2xl border border-white/10 bg-black/20 px-4 py-2.5 text-sm text-slate-100 [html[data-theme=light]_&]:border-slate-200 [html[data-theme=light]_&]:bg-white/60 [html[data-theme=light]_&]:text-slate-800"
|
||||
href={ql.srs_http}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a className="link-tile" href={ql.srs_http} target="_blank" rel="noreferrer">
|
||||
🎞️ SRS HTTP
|
||||
</a>
|
||||
) : null}
|
||||
@@ -928,8 +899,8 @@ export default function Home() {
|
||||
</article>
|
||||
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-4">
|
||||
<h2 className="text-lg font-semibold text-white">{tr("节点状态", "Node Status")}</h2>
|
||||
<div className="mt-4 rounded-2xl border border-white/8 bg-black/20 p-4 text-sm text-slate-300">
|
||||
<h2 className="section-title">{tr("节点状态", "Node Status")}</h2>
|
||||
<div className="panel-inset mt-4">
|
||||
<p>{stack?.mdns_host || "live.local"}</p>
|
||||
<p className="mt-2">IP: {(stack?.ips || []).join(" / ") || "-"}</p>
|
||||
<p className="mt-2">{stack?.machine || "-"}</p>
|
||||
@@ -938,8 +909,9 @@ export default function Home() {
|
||||
</article>
|
||||
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-4">
|
||||
<h2 className="text-lg font-semibold text-[color:var(--text)]">{tr("系统快照", "System Snapshot")}</h2>
|
||||
<div className="mt-4 rounded-2xl border border-white/8 bg-black/20 p-4 text-sm text-slate-300 [html[data-theme=light]_&]:border-slate-200/80 [html[data-theme=light]_&]:bg-white/50 [html[data-theme=light]_&]:text-slate-700">
|
||||
<h2 className="section-title">{tr("系统快照", "System Snapshot")}</h2>
|
||||
<p className="section-lead">{tr("总览页每 10 秒自动刷新服务与监控数据。", "Overview auto-refreshes every 10s.")}</p>
|
||||
<div className="panel-inset mt-4">
|
||||
<p>
|
||||
CPU:{" "}
|
||||
{[snapshot?.cpu_load?.["1m"], snapshot?.cpu_load?.["5m"], snapshot?.cpu_load?.["15m"]]
|
||||
@@ -955,10 +927,10 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
<section className="glass rounded-3xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white">{tr("模块概况", "Module Summary")}</h2>
|
||||
<h2 className="section-title">{tr("模块概况", "Module Summary")}</h2>
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{services.map((item) => (
|
||||
<div key={item.service_id} className="rounded-3xl border border-white/8 bg-black/20 p-5">
|
||||
<div key={item.service_id} className="service-card">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white">{item.label}</h3>
|
||||
@@ -979,12 +951,32 @@ export default function Home() {
|
||||
|
||||
{tab === "business" ? (
|
||||
<section className="grid gap-6 xl:grid-cols-12">
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-12">
|
||||
<h2 className="section-title">{tr("⚡ 快捷配置", "Quick config")}</h2>
|
||||
<p className="section-lead">
|
||||
{tr("跳转配置中心编辑推流与录制相关文件。", "Jump to the config editor for stream keys and URLs.")}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button className="border border-cyan-500/30 bg-cyan-600/80" onClick={() => jumpToConfig("app-config", "youtube.ini")}>
|
||||
youtube.ini
|
||||
</Button>
|
||||
<Button className="border border-cyan-500/30 bg-cyan-600/80" onClick={() => jumpToConfig("app-config", "URL_config.ini")}>
|
||||
URL_config.ini
|
||||
</Button>
|
||||
<Button className="border border-white/15 bg-black/30" onClick={() => jumpToConfig("app-config", "config.ini")}>
|
||||
config.ini
|
||||
</Button>
|
||||
<Button className="border border-white/15 bg-black/30" onClick={() => jumpToConfig("stack-config", "system-stack.env")}>
|
||||
system-stack.env
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-4">
|
||||
<h2 className="text-lg font-semibold text-white">{tr("业务运行时", "Business Runtime")}</h2>
|
||||
<p className="mt-2 text-xs text-slate-400">{tr("这里只放业务脚本本身,Hub 基座可单独运行。", "Only business processes live here.")}</p>
|
||||
<h2 className="section-title">{tr("业务运行时", "Business Runtime")}</h2>
|
||||
<p className="section-lead">{tr("这里只放业务脚本本身,Hub 基座可单独运行。", "Only business processes live here.")}</p>
|
||||
{entries.length > 0 ? (
|
||||
<>
|
||||
<select className="mt-4 w-full rounded-2xl border border-white/12 bg-slate-950/60 px-4 py-3 text-sm text-white" value={currentProcess} onChange={(event) => setCurrentProcess(event.target.value)}>
|
||||
<select className="field-select mt-4" value={currentProcess} onChange={(event) => setCurrentProcess(event.target.value)}>
|
||||
{entries.map((item) => (
|
||||
<option key={item.pm2} value={item.pm2}>{item.label} | {item.script}</option>
|
||||
))}
|
||||
@@ -1006,7 +998,7 @@ export default function Home() {
|
||||
)}
|
||||
</article>
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-8">
|
||||
<h2 className="text-lg font-semibold text-white">{tr("业务日志", "Process Logs")}</h2>
|
||||
<h2 className="section-title">{tr("业务日志", "Process Logs")}</h2>
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<pre className="log-scroll min-h-[22rem] rounded-2xl border border-emerald-500/15 bg-black/55 p-4 font-mono text-[11px] text-emerald-100/95">{recentLog || tr("暂无输出", "No stdout yet.")}</pre>
|
||||
<pre className="log-scroll min-h-[22rem] rounded-2xl border border-rose-500/15 bg-black/55 p-4 font-mono text-[11px] text-rose-100/95">{recentError || tr("暂无报错", "No stderr yet.")}</pre>
|
||||
@@ -1018,16 +1010,23 @@ export default function Home() {
|
||||
{tab === "modules" ? (
|
||||
<section className="grid gap-6 xl:grid-cols-12">
|
||||
<article className="glass rounded-3xl p-6 xl:col-span-8">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{tr("系统模块", "System Modules")}</h2>
|
||||
<p className="mt-2 text-xs text-slate-400">{tr("每个模块都独立启停和降级,避免一个服务异常拖垮整套业务。", "Each module starts and degrades independently.")}</p>
|
||||
<h2 className="section-title">{tr("系统模块", "System Modules")}</h2>
|
||||
<p className="section-lead">{tr("每个模块都独立启停和降级,避免一个服务异常拖垮整套业务。", "Each module starts and degrades independently.")}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button className="border border-white/12 bg-black/20" onClick={() => void refreshServices()}>
|
||||
{tr("刷新服务", "Refresh Services")}
|
||||
</Button>
|
||||
<Button className="border border-violet-500/35 bg-violet-600/70" disabled={busy === "hardware:probe"} onClick={() => void probeHardware()}>
|
||||
{busy === "hardware:probe" ? tr("探测中…", "Probing…") : tr("重探测硬件", "Re-probe HW")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="border border-white/12 bg-black/20" onClick={() => void refreshServices()}>{tr("刷新服务", "Refresh Services")}</Button>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{services.map((item) => (
|
||||
<div key={item.service_id} className="rounded-3xl border border-white/8 bg-black/20 p-5">
|
||||
<div key={item.service_id} className="service-card">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white">{item.label}</h3>
|
||||
|
||||
Reference in New Issue
Block a user