265 lines
11 KiB
TypeScript
265 lines
11 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { usePortalStore } from './store/usePortalStore';
|
|
import { TopRibbon } from './components/TopRibbon';
|
|
import { ServiceCard } from './components/ServiceCard';
|
|
import { SshShelf } from './components/SshShelf';
|
|
import { CommandPalette } from './components/CommandPalette';
|
|
import { AgyTerminalModal } from './components/AgyTerminalModal';
|
|
import { AuthGateCard } from './components/AuthGateModal';
|
|
import { StorybookWorkbench } from './components/StorybookWorkbench';
|
|
import { LAYOUT_PRESETS } from './design-system/tokens';
|
|
import type { ServiceGroup } from './types/telemetry';
|
|
|
|
export const App: React.FC = () => {
|
|
const {
|
|
telemetry,
|
|
toast,
|
|
viewMode,
|
|
layoutPreset,
|
|
fetchTelemetry,
|
|
setShowCmdPalette,
|
|
getEffectiveMode,
|
|
routeMode,
|
|
} = usePortalStore();
|
|
|
|
const [authRedirect, setAuthRedirect] = useState<string | null>(() => {
|
|
if (typeof window === 'undefined') return null;
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get('auth_redirect');
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchTelemetry();
|
|
const timer = setInterval(fetchTelemetry, 3000);
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
e.preventDefault();
|
|
setShowCmdPalette(true);
|
|
}
|
|
};
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => {
|
|
clearInterval(timer);
|
|
window.removeEventListener('keydown', onKeyDown);
|
|
};
|
|
}, [fetchTelemetry, setShowCmdPalette]);
|
|
|
|
const activeModeLabel = useMemo(() => {
|
|
const m = getEffectiveMode();
|
|
if (m === 'lan') return 'LAN 局域网极速直连 (192.168.31.x)';
|
|
if (m === 'home10443') return '10443 家宽公网 HTTPS 直连';
|
|
return '443 免端口 HTTPS 直达 (*.961504.xyz)';
|
|
}, [getEffectiveMode, routeMode]);
|
|
|
|
const gridClass =
|
|
LAYOUT_PRESETS[layoutPreset]?.gridClass || LAYOUT_PRESETS['homepage-3col'].gridClass;
|
|
|
|
const serviceGroups = useMemo<ServiceGroup[]>(() => {
|
|
const t = telemetry;
|
|
return [
|
|
{
|
|
title: '核心工作台与私有云存储',
|
|
subtitle: 'Core Workbench & Storage',
|
|
showRouteHint: true,
|
|
items: [
|
|
{
|
|
id: 'agy',
|
|
title: 'AGY 云端工作台',
|
|
subtitle: 'agy.961504.xyz · ttyd + tmux',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png',
|
|
online: t.services.agy,
|
|
actionType: 'drawer',
|
|
desc: 'M6 Mac mini 上的 24 小时常驻 AI 编程终端,已同步 Google 登录态并锁定新加坡出口,关掉网页后台继续运行。',
|
|
edgeUrl: 'https://agy.961504.xyz',
|
|
homeUrl: 'https://agy.home.961504.xyz:10443',
|
|
lanUrl: 'http://192.168.31.164:7681',
|
|
stats: [
|
|
{ label: 'M6 CPU 负载', value: `${t.mini.cpu_pct}%` },
|
|
{ label: '已用内存', value: `${t.mini.mem_used_gb} / 24 GB` },
|
|
{
|
|
label: 'tmux 会话',
|
|
value: t.mini.agy_tmux ? '常驻运行中' : '待机',
|
|
highlight: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'zspace',
|
|
title: '极空间 Z4 私有云',
|
|
subtitle: '192.168.31.185 · 20TB RAID 存储池',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/synology-drive.png',
|
|
online: t.services.nas,
|
|
desc: '家庭 20TB 大容量存储中心、极影视蓝光影院与手机相册备份,已自动 SMB 挂载至 Mac mini ~/nas。',
|
|
edgeUrl: 'https://z4.home.961504.xyz:10443',
|
|
homeUrl: 'https://z4.home.961504.xyz:10443',
|
|
lanUrl: 'http://192.168.31.185:5055',
|
|
stats: [
|
|
{ label: '剩余可用', value: `${t.nas.free_tb} TB`, highlight: true },
|
|
{ label: '已用容量', value: `${t.nas.used_tb} TB (${t.nas.pct}%)` },
|
|
{ label: 'SMB 挂载', value: t.nas.mounted ? '~/nas 已挂载' : '重连中' },
|
|
],
|
|
},
|
|
{
|
|
id: 'vaultwarden',
|
|
title: 'Vaultwarden 密码保险箱',
|
|
subtitle: 'vw.961504.xyz · Bitwarden 兼容',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/vaultwarden.png',
|
|
online: t.services.vaultwarden,
|
|
actionType: 'backup',
|
|
desc: '自托管全平台加密密码库,支持 iOS / Android / 浏览器插件自动填充,每天凌晨 03:30 自动打包灾备至极空间。',
|
|
edgeUrl: 'https://vw.961504.xyz',
|
|
homeUrl: 'https://vw.home.961504.xyz:10443',
|
|
lanUrl: 'http://192.168.31.164:8222',
|
|
stats: [
|
|
{ label: '容器状态', value: 'Healthy', highlight: true },
|
|
{ label: '最近极空间灾备', value: t.nas.latest_backup },
|
|
{ label: 'Mac SSD 占用', value: `${t.mini.disk_pct}%` },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: '网络网关、AI 专线与外部哨兵',
|
|
subtitle: 'Network Gateway & Cloud Edge',
|
|
showRouteHint: false,
|
|
items: [
|
|
{
|
|
id: 'clash',
|
|
title: 'Mihomo 路由分流面板',
|
|
subtitle: '192.168.31.1:9999 · ShellCrash',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/clash.png',
|
|
online: t.services.clash,
|
|
desc: '小米主路由器透明网关控制台,实时监控全屋设备流量、分流规则与狮城 AI 算力专线锁定状态。',
|
|
edgeUrl: 'https://clash.home.961504.xyz:10443/ui/',
|
|
homeUrl: 'https://clash.home.961504.xyz:10443/ui/',
|
|
lanUrl: 'http://192.168.31.1:9999/ui/',
|
|
stats: [
|
|
{ label: 'AI 锁定策略', value: '狮城节点', highlight: true },
|
|
{
|
|
label: '新加坡延迟',
|
|
value: t.network.sg_delay_ms > 0 ? `${t.network.sg_delay_ms} ms` : '直连',
|
|
},
|
|
{ label: '固定出口 IP', value: t.network.sg_egress_ip },
|
|
],
|
|
},
|
|
{
|
|
id: 'router',
|
|
title: '小米主路由器后台',
|
|
subtitle: '192.168.31.1 · PPPoE 公网网关',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/openwrt.png',
|
|
online: t.services.router,
|
|
desc: '管理家庭联通公网宽带拨号、DHCP 静态 IP 绑定、10443 与 41641 端口转发及全屋内网 Split-DNS。',
|
|
edgeUrl: 'https://router.home.961504.xyz:10443',
|
|
homeUrl: 'https://router.home.961504.xyz:10443',
|
|
lanUrl: 'http://192.168.31.1',
|
|
stats: [
|
|
{ label: '联通公网 IPv4', value: t.network.home_wan_ip, highlight: true },
|
|
{ label: '泛域名 DDNS', value: '*.home.961504.xyz' },
|
|
{ label: '内网网关', value: '192.168.31.1' },
|
|
],
|
|
},
|
|
{
|
|
id: 'uptime',
|
|
title: 'Uptime Kuma 外部哨兵',
|
|
subtitle: 'status.961504.xyz · SG Azure',
|
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png',
|
|
online: t.services.uptime,
|
|
desc: '独立部署在新加坡 Azure 公有云上的 24 小时外部监控哨兵,通过 Tailscale P2P 专线实时探测家庭服务。',
|
|
edgeUrl: 'https://status.961504.xyz',
|
|
homeUrl: 'https://status.961504.xyz',
|
|
lanUrl: 'https://status.961504.xyz',
|
|
stats: [
|
|
{ label: '云端公网 IP', value: '104.43.105.243' },
|
|
{ label: 'Tailscale 专线', value: '100.115.204.51' },
|
|
{ label: '外部探测状态', value: '100% 在线', highlight: true },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}, [telemetry]);
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col">
|
|
{/* Toast Notification */}
|
|
{toast && (
|
|
<div className="fixed bottom-5 right-5 z-50 hl-card px-4 py-3 flex items-center gap-2.5 shadow-2xl">
|
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: 'var(--success)' }} />
|
|
<span className="text-xs font-medium">{toast}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 1. Standard Two-Tier AppShell Header & Telemetry Subheader */}
|
|
<TopRibbon />
|
|
|
|
{/* 2. View Switch: Dashboard vs Storybook Workbench */}
|
|
{viewMode === 'storybook' ? (
|
|
<StorybookWorkbench />
|
|
) : (
|
|
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
|
|
{serviceGroups.map((group) => (
|
|
<section key={group.title} className="space-y-3">
|
|
<div className="flex items-center justify-between px-0.5">
|
|
<div className="flex items-center gap-2">
|
|
<h2
|
|
className="text-xs font-semibold uppercase tracking-wider"
|
|
style={{ color: 'var(--text-muted)' }}
|
|
>
|
|
{group.title}
|
|
</h2>
|
|
<span
|
|
className="text-[11px] font-mono-num"
|
|
style={{ color: 'var(--text-subtle)' }}
|
|
>
|
|
· {group.subtitle}
|
|
</span>
|
|
</div>
|
|
{group.showRouteHint && (
|
|
<span
|
|
className="text-[11px] font-mono-num hidden sm:inline"
|
|
style={{ color: 'var(--text-subtle)' }}
|
|
>
|
|
当前路由模式: {activeModeLabel}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className={`grid ${gridClass}`}>
|
|
{group.items.map((card) => (
|
|
<ServiceCard key={card.id} card={card} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
))}
|
|
|
|
{/* 3. Bottom SSH Quick Copy & Maintenance Shelf */}
|
|
<SshShelf />
|
|
</main>
|
|
)}
|
|
|
|
{/* 4. Global Cmd+K Command Palette */}
|
|
<CommandPalette />
|
|
|
|
{/* 5. Embedded AGY Cloud Terminal Modal */}
|
|
<AgyTerminalModal />
|
|
|
|
{/* 6. Custom SSO Auth Gate Modal (Replaces Browser Native Basic Auth Popup!) */}
|
|
{authRedirect && (
|
|
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<AuthGateCard
|
|
targetUrl={authRedirect}
|
|
onSuccess={() => {
|
|
window.location.href = authRedirect;
|
|
}}
|
|
onCancel={() => {
|
|
setAuthRedirect(null);
|
|
window.history.replaceState({}, '', '/');
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|