diff --git a/config/system-stack.env.example b/config/system-stack.env.example
index cf808c0..4b9a2e0 100644
--- a/config/system-stack.env.example
+++ b/config/system-stack.env.example
@@ -47,6 +47,15 @@ WIFI_SSID=live
WIFI_PSK=12345678
DISABLE_IPV6=1
+# ws-scrcpy 克隆地址(可改为 NetrisTV 上游:https://github.com/NetrisTV/ws-scrcpy.git)
+# ANDROID_PANEL_GIT_URL=https://github.com/baixin1228/web-scrcpy.git
+
+# 可选:Docker Neko 浏览器桌面(m1k1o/neko),与 SRS 8080 错开端口
+ENABLE_NEKO=0
+NEKO_HTTP_PORT=9200
+NEKO_USER_PASS=live
+NEKO_ADMIN_PASS=admin
+
# Android gateway mode
ANDROID_GATEWAY_NAME=live-android-gateway
ANDROID_UPLINK_IF=eth0
diff --git a/scripts/linux/install_business.sh b/scripts/linux/install_business.sh
index af97be6..8c41e8d 100644
--- a/scripts/linux/install_business.sh
+++ b/scripts/linux/install_business.sh
@@ -27,6 +27,7 @@ install_live_console_service
configure_sudoers
configure_mdns
install_browser_stack
+install_neko_browser
prepare_srs
run_hardware_probe
print_summary "business"
diff --git a/scripts/linux/install_hub.sh b/scripts/linux/install_hub.sh
index 2649f5f..60cfa15 100644
--- a/scripts/linux/install_hub.sh
+++ b/scripts/linux/install_hub.sh
@@ -26,6 +26,7 @@ configure_mdns
configure_wifi_profile
install_shellcrash
install_browser_stack
+install_neko_browser
install_webtty
install_cockpit
install_netdata
diff --git a/scripts/linux/service_ctl.sh b/scripts/linux/service_ctl.sh
index eab2808..3756e5c 100644
--- a/scripts/linux/service_ctl.sh
+++ b/scripts/linux/service_ctl.sh
@@ -11,6 +11,7 @@ HW_ENV="$CONFIG_DIR/hardware.env"
SRS_COMPOSE="$ROOT_DIR/services/srs/docker-compose.yml"
HOMEPAGE_COMPOSE="$ROOT_DIR/services/homepage/docker-compose.yml"
FILEBROWSER_COMPOSE="$ROOT_DIR/services/filebrowser/docker-compose.yml"
+NEKO_COMPOSE="$ROOT_DIR/services/neko/docker-compose.yml"
BROWSER_LAUNCHER="$ROOT_DIR/scripts/linux/launch_browser.sh"
[ -f "$STACK_ENV" ] && set -a && . "$STACK_ENV" && set +a
@@ -39,6 +40,7 @@ ANDROID_UPLINK_IF="${ANDROID_UPLINK_IF:-eth0}"
ANDROID_DOWNLINK_IF="${ANDROID_DOWNLINK_IF:-}"
ANDROID_GATEWAY_CIDR="${ANDROID_GATEWAY_CIDR:-192.168.51.1/24}"
ANDROID_PANEL_PORT="${ANDROID_PANEL_PORT:-5000}"
+NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}"
CONSOLE_BASE_URL="http://${HOSTNAME_ALIAS}.local:${PORT}"
kv() {
@@ -416,7 +418,7 @@ service_android_panel_status() {
kv host "${HOSTNAME_ALIAS}.local"
kv port "$ANDROID_PANEL_PORT"
kv url "${CONSOLE_BASE_URL}/android"
- kv detail "android-center=${CONSOLE_BASE_URL}/android;raw-web-scrcpy=http://${HOSTNAME_ALIAS}.local:${ANDROID_PANEL_PORT}"
+ kv detail "android-center=${CONSOLE_BASE_URL}/android;ws-scrcpy=http://${HOSTNAME_ALIAS}.local:${ANDROID_PANEL_PORT};git=ANDROID_PANEL_GIT_URL"
}
service_android_panel_start() {
@@ -459,6 +461,66 @@ service_chromium_stop() {
kv message "Chromium stopped"
}
+service_neko_status() {
+ if [ ! -f "$NEKO_COMPOSE" ]; then
+ kv available 0
+ kv running 0
+ kv status unavailable
+ kv detail "services/neko/docker-compose.yml missing"
+ return 0
+ fi
+ if ! have docker; then
+ kv available 0
+ kv running 0
+ kv status unavailable
+ kv detail "docker not installed"
+ return 0
+ fi
+ kv available 1
+ local state
+ state="$(docker_container_status live-neko-chromium || true)"
+ kv running "$(echo "$state" | grep -qi running && echo 1 || echo 0)"
+ kv status "$(echo "$state" | grep -qi running && echo online || echo stopped)"
+ kv host "${HOSTNAME_ALIAS}.local"
+ kv port "$NEKO_HTTP_PORT"
+ kv url "http://${HOSTNAME_ALIAS}.local:${NEKO_HTTP_PORT}"
+ kv detail "m1k1o/neko Chromium in Docker; ENABLE_NEKO=1 + install_neko_browser or Modules start"
+}
+
+service_neko_start() {
+ if [ ! -f "$NEKO_COMPOSE" ]; then
+ kv message "Neko compose missing"
+ return 0
+ fi
+ cmd="$(compose_bin)" || {
+ kv message "docker compose unavailable"
+ return 0
+ }
+ if [ "$cmd" = "docker compose" ]; then
+ docker compose -f "$NEKO_COMPOSE" up -d
+ else
+ docker-compose -f "$NEKO_COMPOSE" up -d
+ fi
+ kv message "Neko stack started"
+}
+
+service_neko_stop() {
+ if [ ! -f "$NEKO_COMPOSE" ]; then
+ kv message "Neko compose missing"
+ return 0
+ fi
+ cmd="$(compose_bin)" || {
+ kv message "docker compose unavailable"
+ return 0
+ }
+ if [ "$cmd" = "docker compose" ]; then
+ docker compose -f "$NEKO_COMPOSE" down
+ else
+ docker-compose -f "$NEKO_COMPOSE" down
+ fi
+ kv message "Neko stack stopped"
+}
+
service_android_gateway_status() {
if ! have nmcli || [ -z "$ANDROID_DOWNLINK_IF" ]; then
kv available 0
diff --git a/services/neko/docker-compose.yml b/services/neko/docker-compose.yml
new file mode 100644
index 0000000..f2ab2c9
--- /dev/null
+++ b/services/neko/docker-compose.yml
@@ -0,0 +1,15 @@
+# 可选:m1k1o/neko 浏览器桌面(ARM64/x86)。需在 system-stack.env 设 ENABLE_NEKO=1 并由安装脚本拉起。
+# 默认映射宿主机 NEKO_HTTP_PORT(默认 9200)→ 容器 8080,避免与 SRS 8080 冲突。
+services:
+ neko:
+ image: ghcr.io/m1k1o/neko/chromium:latest
+ container_name: live-neko-chromium
+ restart: unless-stopped
+ shm_size: "2gb"
+ environment:
+ NEKO_PASSWORD: "${NEKO_USER_PASS:-live}"
+ NEKO_PASSWORD_ADMIN: "${NEKO_ADMIN_PASS:-admin}"
+ NEKO_BIND: "0.0.0.0:8080"
+ ports:
+ - "${NEKO_HTTP_PORT:-9200}:8080"
+ - "52000-52100:52000-52100/udp"
diff --git a/src/control_plane.py b/src/control_plane.py
index 5772593..b31d126 100644
--- a/src/control_plane.py
+++ b/src/control_plane.py
@@ -65,8 +65,19 @@ SERVICE_SPECS: tuple[ServiceSpec, ...] = (
ServiceSpec("homepage", "Homepage", "admin", "Start page for all LAN services"),
ServiceSpec("netdata", "Netdata", "monitor", "System metrics and health monitoring"),
ServiceSpec("adb", "ADB", "android", "Android bridge and device inspection"),
- ServiceSpec("android_panel", "Android Web Panel", "android", "Browser-based Android control with web-scrcpy"),
- ServiceSpec("chromium", "Chromium", "browser", "Persistent browser profile launcher"),
+ ServiceSpec(
+ "android_panel",
+ "ws-scrcpy / Web Android",
+ "android",
+ "Browser ADB control (ws-scrcpy stack; clone URL configurable in system-stack.env)",
+ ),
+ ServiceSpec("chromium", "Chromium 浏览器", "browser", "本机 Chromium + 持久化配置 + Remote DevTools(非 Docker)"),
+ ServiceSpec(
+ "neko",
+ "Neko 浏览器 (Docker)",
+ "browser",
+ "可选:Docker 内嵌 Chromium 桌面,适合远程开 YouTube Studio;ENABLE_NEKO=1 时安装",
+ ),
ServiceSpec(
"android_gateway",
"Android Gateway",
@@ -449,6 +460,13 @@ def stack_summary() -> dict:
web_port = env.get("PORT", os.environ.get("PORT", "8001"))
homepage_port = env.get("HOMEPAGE_PORT", "80")
mdns_host = f"{hostname}.local"
+ webtty = env.get("WEBTTY_PORT", "7681")
+ fb = env.get("FILEBROWSER_PORT", "8082")
+ netd = env.get("NETDATA_PORT", "19999")
+ srs_http = env.get("SRS_HTTP_PORT", "8080")
+ android_panel = env.get("ANDROID_PANEL_PORT", "5000")
+ chrome_dbg = env.get("BROWSER_REMOTE_DEBUG_PORT", "9222")
+ neko_port = env.get("NEKO_HTTP_PORT", "9200")
return {
"hostname": hostname,
"mdns_host": mdns_host,
@@ -460,6 +478,16 @@ def stack_summary() -> dict:
"dashboard_url": f"http://{mdns_host}" if homepage_port in {"80", ""} else f"http://{mdns_host}:{homepage_port}",
"control_url": f"http://{mdns_host}:{web_port}",
"stack_env_present": STACK_ENV.is_file(),
+ "quick_links": {
+ "control_plane": f"http://{mdns_host}:{web_port}",
+ "webtty": f"http://{mdns_host}:{webtty}",
+ "filebrowser": f"http://{mdns_host}:{fb}",
+ "netdata": f"http://{mdns_host}:{netd}",
+ "srs_http": f"http://{mdns_host}:{srs_http}",
+ "ws_scrcpy": f"http://{mdns_host}:{android_panel}",
+ "chromium_remote_debug": f"http://{mdns_host}:{chrome_dbg}",
+ "neko_browser": f"http://{mdns_host}:{neko_port}",
+ },
}
diff --git a/web-console/app/globals.css b/web-console/app/globals.css
index 7b936e1..7ffb47a 100644
--- a/web-console/app/globals.css
+++ b/web-console/app/globals.css
@@ -11,6 +11,15 @@
--ok: #34d399;
--text: #e8edf7;
--muted: #94a3b8;
+ --ring-offset: #060912;
+}
+
+html[data-theme="light"] {
+ --bg-deep: #f1f5f9;
+ --bg-card: rgba(255, 255, 255, 0.88);
+ --text: #0f172a;
+ --muted: #64748b;
+ --ring-offset: #f1f5f9;
}
body {
@@ -18,6 +27,31 @@ body {
background: var(--bg-deep);
min-height: 100vh;
font-family: var(--font-geist-sans), system-ui, sans-serif;
+ transition: background-color 0.35s ease, color 0.25s ease;
+}
+
+.app-bg-layer {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ opacity: 0.92;
+ pointer-events: none;
+ background: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(34, 211, 238, 0.22), transparent),
+ radial-gradient(ellipse 60% 40% at 100% 0%, rgba(167, 139, 250, 0.18), transparent),
+ radial-gradient(ellipse 50% 30% at 0% 100%, rgba(59, 130, 246, 0.12), transparent);
+ transition: opacity 0.35s ease;
+}
+
+html[data-theme="light"] .app-bg-layer {
+ opacity: 0.75;
+ background: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(14, 165, 233, 0.12), transparent),
+ radial-gradient(ellipse 60% 40% at 100% 0%, rgba(139, 92, 246, 0.1), transparent),
+ radial-gradient(ellipse 50% 30% at 0% 100%, rgba(59, 130, 246, 0.08), transparent);
+}
+
+html[data-theme="light"] .chart-surface {
+ border-color: rgba(15, 23, 42, 0.12) !important;
+ background: rgba(255, 255, 255, 0.65) !important;
}
::selection {
@@ -37,7 +71,19 @@ body {
}
.btn-focus {
- @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50 focus-visible:ring-offset-2 focus-visible:ring-offset-[#060912];
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50 focus-visible:ring-offset-2;
+ --tw-ring-offset-color: var(--ring-offset);
+ }
+}
+
+/* H5 底部导航安全区 */
+.safe-pb-nav {
+ padding-bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
+}
+
+@media (min-width: 768px) {
+ .safe-pb-nav {
+ padding-bottom: 4rem;
}
}
diff --git a/web-console/app/layout.tsx b/web-console/app/layout.tsx
index e1ee693..09ff9b2 100644
--- a/web-console/app/layout.tsx
+++ b/web-console/app/layout.tsx
@@ -25,15 +25,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
-
+
{children}
diff --git a/web-console/app/page.tsx b/web-console/app/page.tsx
index c711b8d..32c9830 100644
--- a/web-console/app/page.tsx
+++ b/web-console/app/page.tsx
@@ -2,7 +2,10 @@
import { type ChangeEvent, type MouseEvent, useCallback, useEffect, useState } from "react";
+import OverviewCharts from "@/components/OverviewCharts";
+
type Lang = "zh" | "en";
+type ThemeMode = "dark" | "light";
type TabKey = "overview" | "business" | "modules" | "shellcrash" | "android" | "config";
type LoadAvg = { "1m"?: number; "5m"?: number; "15m"?: number };
@@ -37,6 +40,7 @@ type StackSummary = {
ips?: string[];
dashboard_url?: string;
control_url?: string;
+ quick_links?: Record;
};
type ServiceItem = {
service_id: string;
@@ -164,17 +168,21 @@ function Button({
className,
disabled,
onClick,
+ title,
}: {
children: React.ReactNode;
className?: string;
disabled?: boolean;
onClick?: () => void;
+ title?: string;
}) {
return (
@@ -184,6 +192,7 @@ function Button({
export default function Home() {
const base = apiBase();
const [lang, setLang] = useState("zh");
+ const [theme, setTheme] = useState("dark");
const [tab, setTab] = useState("overview");
const tr = useCallback((zh: string, en: string) => (lang === "zh" ? zh : en), [lang]);
@@ -231,12 +240,14 @@ export default function Home() {
const otherRoots = roots.filter((item) => !SHELLCRASH_ROOT_IDS.has(item.root_id));
const shellcrashService = services.find((item) => item.service_id === "shellcrash") ?? null;
const androidService = services.find((item) => item.service_id === "android_panel") ?? null;
+ const ql = stack?.quick_links ?? {};
const rawAndroidPanelUrl =
- androidService?.host && androidService.port
+ ql.ws_scrcpy ||
+ (androidService?.host && androidService.port
? `http://${androidService.host}:${androidService.port}`
: stack?.mdns_host
? `http://${stack.mdns_host}:5000`
- : "";
+ : "");
const androidResolution = parseResolution(androidOverview?.resolution);
const notify = useCallback((message: string) => {
@@ -404,8 +415,15 @@ export default function Home() {
else if (isTabKey(nextTab)) setTab(nextTab);
if (nextLang === "zh" || nextLang === "en") setLang(nextLang);
else if (storedLang === "zh" || storedLang === "en") setLang(storedLang);
+ const storedTheme = window.localStorage.getItem("live-console-theme");
+ if (storedTheme === "light" || storedTheme === "dark") setTheme(storedTheme);
}, []);
+ useEffect(() => {
+ document.documentElement.setAttribute("data-theme", theme);
+ window.localStorage.setItem("live-console-theme", theme);
+ }, [theme]);
+
useEffect(() => {
syncUrl(tab, lang);
}, [lang, syncUrl, tab]);
@@ -672,8 +690,18 @@ export default function Home() {
const activeRoots = tab === "shellcrash" ? shellRoots : otherRoots;
+ const tabEmoji: Record = {
+ overview: "📊",
+ business: "🎬",
+ modules: "🧩",
+ shellcrash: "🛡️",
+ android: "🤖",
+ config: "⚙️",
+ };
+
return (
-
+ <>
+
{toast ? (
{toast}
@@ -686,13 +714,13 @@ export default function Home() {
{tr("超级业务中心 / 模块控制台", "Super Hub / Control Plane")}
-
- {tr("Linux 超级业务中心", "Linux Super Business Center")}
+
+ {tr("无人直播 · Linux 超级业务中心", "Unmanned Live · Linux Super Hub")}
{tr(
- "系统服务、业务模块、ShellCrash 配置与安卓控制保持解耦,一个模块异常不应拖垮整个平台。",
- "Services, business modules, ShellCrash configs and Android control stay decoupled.",
+ "抖音 → YouTube / HDMI 转播、ws-scrcpy、Chromium 与可选 Neko 浏览器、ShellCrash 等模块解耦,单点故障不拖垮整机。",
+ "Douyin→YouTube/HDMI relay, ws-scrcpy, Chromium & optional Neko, ShellCrash — decoupled modules.",
)}
@@ -716,6 +744,20 @@ export default function Home() {
>
EN
+
+
@@ -764,20 +806,123 @@ export default function Home() {
<>
- {tr("常用入口", "Quick Links")}
-
+
+ {tr("🛰️ 流媒体与桌面入口", "Streaming & desktop")}
+
+
+ {tr(
+ "ws-scrcpy:浏览器里控安卓;Chromium:本机+油猴+RemoteDebug;Neko:可选 Docker 远程桌面开 YouTube。",
+ "ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222; Neko: optional Docker desktop.",
+ )}
+
+
@@ -793,8 +938,8 @@ export default function Home() {
- {tr("系统快照", "System Snapshot")}
-
+
{tr("系统快照", "System Snapshot")}
+
CPU:{" "}
{[snapshot?.cpu_load?.["1m"], snapshot?.cpu_load?.["5m"], snapshot?.cpu_load?.["15m"]]
@@ -805,6 +950,7 @@ export default function Home() {
Disk: {formatBytes(snapshot?.disk?.free)} / {formatBytes(snapshot?.disk?.total)}
Netdata: {snapshot?.netdata?.available ? snapshot.netdata.version || "online" : "offline"}
+
@@ -1170,10 +1316,10 @@ export default function Home() {
{rawAndroidPanelUrl ? (
) : null}
@@ -1237,5 +1383,37 @@ export default function Home() {
) : null}
+
+
+ >
);
}
diff --git a/web-console/components/OverviewCharts.tsx b/web-console/components/OverviewCharts.tsx
new file mode 100644
index 0000000..82bd82d
--- /dev/null
+++ b/web-console/components/OverviewCharts.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+export type SnapshotLite = {
+ cpu_load?: { "1m"?: number; "5m"?: number; "15m"?: number };
+ disk?: { total?: number; free?: number; used?: number };
+ memory?: { total?: number; available?: number; free?: number };
+};
+
+export default function OverviewCharts({
+ snapshot,
+ theme,
+ tr,
+}: {
+ snapshot: SnapshotLite | null;
+ theme: "dark" | "light";
+ tr: (zh: string, en: string) => string;
+}) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el || !snapshot) return;
+ let disposed = false;
+ let chart: import("echarts").ECharts | null = null;
+ const fg = theme === "light" ? "#0f172a" : "#e2e8f0";
+ const muted = theme === "light" ? "#64748b" : "#94a3b8";
+ const gridLine = theme === "light" ? "rgba(15,23,42,0.1)" : "rgba(148,163,184,0.12)";
+
+ import("echarts").then((echarts) => {
+ if (disposed || !ref.current) return;
+ chart = echarts.init(ref.current, undefined, { renderer: "canvas" });
+ const l1 = snapshot.cpu_load?.["1m"] ?? 0;
+ const l5 = snapshot.cpu_load?.["5m"] ?? 0;
+ const l15 = snapshot.cpu_load?.["15m"] ?? 0;
+ const memTotal = snapshot.memory?.total || 1;
+ const memAvail = snapshot.memory?.available ?? snapshot.memory?.free ?? 0;
+ const memUsed = Math.max(0, memTotal - memAvail);
+ const diskTotal = snapshot.disk?.total || 1;
+ const diskFree = snapshot.disk?.free || 0;
+ const diskUsed = Math.max(0, diskTotal - diskFree);
+
+ chart.setOption({
+ animationDurationUpdate: 450,
+ textStyle: { color: fg },
+ title: {
+ text: tr("无人直播节点 · 资源", "Unmanned live · resources"),
+ left: 12,
+ top: 8,
+ textStyle: { color: fg, fontSize: 12, fontWeight: 600 },
+ },
+ tooltip: { trigger: "axis" },
+ grid: { left: 52, right: "48%", top: 52, bottom: 36 },
+ xAxis: {
+ type: "category",
+ data: ["1m", "5m", "15m"],
+ axisLabel: { color: muted, fontSize: 10 },
+ axisLine: { lineStyle: { color: gridLine } },
+ },
+ yAxis: {
+ type: "value",
+ name: tr("负载", "Load"),
+ nameTextStyle: { color: muted, fontSize: 10 },
+ axisLabel: { color: muted, fontSize: 10 },
+ splitLine: { lineStyle: { color: gridLine } },
+ },
+ series: [
+ {
+ name: tr("CPU", "CPU"),
+ type: "bar",
+ data: [l1, l5, l15],
+ barMaxWidth: 28,
+ itemStyle: {
+ borderRadius: [6, 6, 0, 0],
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: "#22d3ee" },
+ { offset: 1, color: "#6366f1" },
+ ]),
+ },
+ },
+ {
+ name: tr("内存", "RAM"),
+ type: "pie",
+ radius: ["26%", "40%"],
+ center: ["78%", "38%"],
+ label: { color: fg, fontSize: 9 },
+ data: [
+ { value: memUsed, name: tr("已用", "Used") },
+ { value: Math.max(0, memAvail), name: tr("可用", "Free") },
+ ],
+ },
+ {
+ name: tr("磁盘", "Disk"),
+ type: "pie",
+ radius: ["20%", "32%"],
+ center: ["78%", "72%"],
+ label: { color: fg, fontSize: 9 },
+ data: [
+ { value: diskUsed, name: tr("已用", "Used") },
+ { value: Math.max(0, diskFree), name: tr("空闲", "Free") },
+ ],
+ },
+ ],
+ });
+ });
+
+ const ro = new ResizeObserver(() => chart?.resize());
+ ro.observe(el);
+ return () => {
+ disposed = true;
+ ro.disconnect();
+ chart?.dispose();
+ };
+ }, [snapshot, theme, tr]);
+
+ if (!snapshot) return null;
+
+ return (
+
+
{tr("ECharts 动态图表", "ECharts live charts")}
+
+
+ );
+}
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 3724434..76f46dc 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -8,6 +8,7 @@
"name": "web-console",
"version": "0.1.0",
"dependencies": {
+ "echarts": "^5.6.0",
"next": "14.2.35",
"react": "^18",
"react-dom": "^18"
@@ -1906,6 +1907,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
+ "node_modules/echarts/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -5999,6 +6016,21 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ },
+ "node_modules/zrender/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
}
}
}
diff --git a/web-console/package.json b/web-console/package.json
index a33fc23..2491bdc 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -9,19 +9,20 @@
"lint": "next lint"
},
"dependencies": {
+ "echarts": "^5.6.0",
+ "next": "14.2.35",
"react": "^18",
- "react-dom": "^18",
- "next": "14.2.35"
+ "react-dom": "^18"
},
"devDependencies": {
- "typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "14.2.35",
"postcss": "^8",
"tailwindcss": "^3.4.1",
- "eslint": "^8",
- "eslint-config-next": "14.2.35"
+ "typescript": "^5"
},
"packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b"
}