This website requires JavaScript.
0
Issues
Pull Requests
Milestones
Explore
0
Create…
New Repository
New Migration
New Organization
admin
Signed in as
admin
Profile
Starred
Subscriptions
Settings
Help
Site Administration
Sign Out
admin
/
homelab
Watching
1
Participating and mentions
Receive notifications from this repository when participating or mentioned.
All activity
Receive notifications for all events.
Ignore
Never receive notifications from this repository.
Custom
Select events you want to be notified of, in addition to participating and mentions.
Star
0
Fork
0
Notifications
Issues
Pull Requests
Releases
Cancel
Confirm
Code
Issues
Pull Requests
Actions
Packages
Projects
Releases
Wiki
Activity
Settings
Files
homelab
/
stacks
/
portal
/
or
Cancel
Edit File
Preview
Preview Changes
Spaces
Tabs
2
4
8
Wrap
No wrap
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>HomeLab Console · 家庭云边中控台</title> <link rel="icon" href="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> <script src="https://unpkg.com/lucide@latest"></script> <style> /* ========================================================================= HomeLab Console V3 Architecture: gethomepage.dev (Top Ribbon + 3-Col Rows + Inline Widgets) Design System: shadcn/ui + Raycast Command Palette + 4 Official Themes ========================================================================= */ :root, [data-theme="zinc-dark"] { --bg-page: #09090b; --bg-card: #121215; --bg-widget: #16161a; --bg-muted: #18181b; --bg-hover: #1c1c21; --text-main: #f4f4f5; --text-muted: #a1a1aa; --text-subtle: #71717a; --border-color: #27272a; --border-subtle: rgba(255, 255, 255, 0.06); --border-hover: #3f3f46; --primary: #fafafa; --primary-fg: #09090b; --accent: #3b82f6; --accent-soft: rgba(59, 130, 246, 0.14); --success: #10b981; --success-soft: rgba(16, 185, 129, 0.14); --card-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.5); } [data-theme="zinc-light"] { --bg-page: #f8fafc; --bg-card: #ffffff; --bg-widget: #f8fafc; --bg-muted: #f1f5f9; --bg-hover: #ffffff; --text-main: #0f172a; --text-muted: #64748b; --text-subtle: #94a3b8; --border-color: #e2e8f0; --border-subtle: #f1f5f9; --border-hover: #cbd5e1; --primary: #0f172a; --primary-fg: #ffffff; --accent: #2563eb; --accent-soft: rgba(37, 99, 235, 0.08); --success: #10b981; --success-soft: rgba(16, 185, 129, 0.10); --card-shadow: 0 1px 3px 0 rgba(15, 23, 42, 0.04), 0 1px 2px -1px rgba(15, 23, 42, 0.03); } [data-theme="catppuccin"] { --bg-page: #11111b; --bg-card: #1e1e2e; --bg-widget: #181825; --bg-muted: #313244; --bg-hover: #242438; --text-main: #cdd6f4; --text-muted: #a6adc8; --text-subtle: #6c7086; --border-color: #313244; --border-subtle: rgba(205, 214, 244, 0.06); --border-hover: #45475a; --primary: #cba6f7; --primary-fg: #11111b; --accent: #89b4fa; --accent-soft: rgba(137, 180, 250, 0.15); --success: #a6e3a1; --success-soft: rgba(166, 227, 161, 0.15); --card-shadow: 0 4px 16px -4px rgba(17, 17, 27, 0.6); } [data-theme="nord"] { --bg-page: #242933; --bg-card: #2e3440; --bg-widget: #292e39; --bg-muted: #3b4252; --bg-hover: #353c4a; --text-main: #eceff4; --text-muted: #d8dee9; --text-subtle: #81a1c1; --border-color: #3b4252; --border-subtle: rgba(236, 239, 244, 0.06); --border-hover: #4c566a; --primary: #88c0d0; --primary-fg: #2e3440; --accent: #88c0d0; --accent-soft: rgba(136, 192, 208, 0.15); --success: #a3be8c; --success-soft: rgba(163, 190, 140, 0.15); --card-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.35); } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: var(--bg-page); color: var(--text-main); transition: background-color 0.2s ease, color 0.2s ease; -webkit-font-smoothing: antialiased; } .font-mono-num { font-family: 'JetBrains Mono', 'SF Mono', Menlo, monospace; font-variant-numeric: tabular-nums; } .hl-card { background-color: var(--bg-card); border: 1px solid var(--border-color); box-shadow: var(--card-shadow); border-radius: 0.75rem; transition: border-color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease; } .hl-card-interactive:hover { border-color: var(--border-hover); background-color: var(--bg-hover); transform: translateY(-1px); } .hl-widget-footer { background-color: var(--bg-widget); border-top: 1px solid var(--border-color); } .hl-pill { background-color: var(--bg-card); border: 1px solid var(--border-color); border-radius: 0.5rem; } .hl-muted { background-color: var(--bg-muted); } .hl-border { border-color: var(--border-color); } .hl-btn-primary { background-color: var(--primary); color: var(--primary-fg); font-weight: 500; border-radius: 0.5rem; transition: opacity 0.15s ease; } .hl-btn-primary:hover { opacity: 0.9; } .hl-btn-outline { background-color: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color); border-radius: 0.5rem; transition: background-color 0.15s ease, border-color 0.15s ease; } .hl-btn-outline:hover { background-color: var(--bg-muted); border-color: var(--border-hover); } .hl-bar-fill { background-color: var(--accent); transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1); } .hl-bar-fill-green { background-color: var(--success); transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1); } [v-cloak] { display: none; } </style> </head> <body class="min-h-screen"> <div id="app" v-cloak class="min-h-screen flex flex-col"> <!-- Toast Notification --> <div v-if="toast" class="fixed bottom-5 right-5 z-50 hl-card px-4 py-3 flex items-center gap-2.5 shadow-2xl"> <span class="w-2 h-2 rounded-full" style="background-color: var(--success)"></span> <span class="text-xs font-medium">{{ toast }}</span> </div> <!-- ======================================================================= 1. gethomepage.dev Style Top Utility & Resources Ribbon ======================================================================= --> <header class="border-b hl-border sticky top-0 z-30 backdrop-blur-md" style="background-color: color-mix(in srgb, var(--bg-page) 86%, transparent);"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex flex-wrap items-center justify-between gap-3"> <!-- Left: Brand + Live System Micro-Gauges Ribbon --> <div class="flex flex-wrap items-center gap-2.5 sm:gap-3"> <div class="flex items-center gap-2.5 pr-1"> <img src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png" alt="Mac mini" class="w-6 h-6 rounded" /> <span class="font-semibold text-sm tracking-tight">HomeLab</span> <span class="w-2 h-2 rounded-full" style="background-color: var(--success)" title="全屋 4 节点在线"></span> </div> <!-- Compact Resource Ribbon (Homepage Top Widget Style) --> <div class="hidden xl:flex items-center gap-2 text-xs font-mono-num"> <!-- CPU Gauge --> <div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="Apple M6 12核处理器负载"> <span style="color: var(--text-muted)">M6 CPU</span> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden"> <div class="h-full hl-bar-fill" :style="{ width: Math.max(5, telemetry.mini.cpu_pct) + '%' }"></div> </div> <span class="font-medium">{{ telemetry.mini.cpu_pct }}%</span> </div> <!-- RAM Gauge --> <div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="Mac mini 24GB 统一内存"> <span style="color: var(--text-muted)">RAM</span> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden"> <div class="h-full hl-bar-fill" :style="{ width: Math.max(5, telemetry.mini.mem_pct) + '%' }"></div> </div> <span class="font-medium">{{ telemetry.mini.mem_used_gb }}G</span> </div> <!-- ZSpace 20TB Gauge --> <div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="极空间 Z4 20TB 存储阵列"> <span style="color: var(--text-muted)">NAS 余量</span> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden"> <div class="h-full hl-bar-fill-green" :style="{ width: Math.max(5, telemetry.nas.pct) + '%' }"></div> </div> <span class="font-medium" style="color: var(--success)">{{ telemetry.nas.free_tb }} TB</span> </div> <!-- SG AI Line Gauge --> <div class="hl-pill px-2.5 py-1 flex items-center gap-1.5" title="AGY 锁定新加坡 Azure 专线出口"> <span class="w-1.5 h-1.5 rounded-full" style="background-color: var(--success)"></span> <span style="color: var(--text-muted)">SG 专线</span> <span class="font-medium">{{ telemetry.network.sg_delay_ms > 0 ? telemetry.network.sg_delay_ms + 'ms' : '直连' }}</span> </div> </div> </div> <!-- Right: Search (Cmd+K), Route Switcher, Theme Switcher, Terminal Button --> <div class="flex items-center gap-2"> <!-- Cmd+K Command Palette Trigger --> <button @click="openCmdPalette" class="hl-btn-outline px-2.5 py-1 text-xs flex items-center gap-3" title="按 Cmd+K 或 / 键唤起全局命令面板" > <span class="flex items-center gap-1.5" style="color: var(--text-muted)"> <i data-lucide="search" class="w-3.5 h-3.5"></i> <span class="hidden sm:inline">搜索或执行指令...</span> </span> <kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">⌘K</kbd> </button> <!-- Route Mode Segmented Switcher --> <div class="hidden md:flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs"> <button v-for="m in modes" :key="m.id" @click="routeMode = m.id" class="px-2 py-1 rounded-md font-medium transition-all" :style="routeMode === m.id ? 'background-color: var(--bg-card); color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.08);' : 'color: var(--text-muted);'" > {{ m.label }} </button> </div> <!-- Theme Selector --> <div class="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs"> <button v-for="t in themes" :key="t.id" @click="setTheme(t.id)" class="px-2 py-1 rounded-md font-medium transition-all flex items-center gap-1.5" :style="currentTheme === t.id ? 'background-color: var(--bg-card); color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.08);' : 'color: var(--text-muted);'" :title="t.label" > <span class="w-2 h-2 rounded-full border border-black/10" :style="{ backgroundColor: t.dot }"></span> <span class="hidden lg:inline">{{ t.label }}</span> </button> </div> <!-- Embedded AGY Terminal Trigger --> <button @click="showAgyModal = true" class="hl-btn-primary px-3 py-1.5 text-xs flex items-center gap-1.5" > <i data-lucide="terminal" class="w-3.5 h-3.5"></i> <span>AGY 终端</span> </button> </div> </div> </header> <!-- ======================================================================= 2. Main Categorized 3-Column Rows (gethomepage.dev Signature Layout) Each card embeds a 3-cell Live Telemetry Widget Block at its footer! ======================================================================= --> <main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7"> <!-- Category Group Loop --> <section v-for="group in serviceGroups" :key="group.title" class="space-y-3"> <!-- Group Header --> <div class="flex items-center justify-between px-0.5"> <div class="flex items-center gap-2"> <h2 class="text-xs font-semibold uppercase tracking-wider" style="color: var(--text-muted)"> {{ group.title }} </h2> <span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">· {{ group.subtitle }}</span> </div> <span v-if="group.showRouteHint" class="text-[11px] font-mono-num hidden sm:inline" style="color: var(--text-subtle)"> 当前路由模式: {{ activeModeLabel }} </span> </div> <!-- 3-Column Symmetric Service Cards Grid --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <a v-for="card in group.items" :key="card.id" :href="resolveUrl(card)" target="_blank" rel="noopener noreferrer" class="hl-card hl-card-interactive flex flex-col justify-between overflow-hidden group cursor-pointer" > <!-- Card Top: Authentic Brand Icon + Title + Status Dot + Description --> <div class="p-4 sm:p-5 space-y-3"> <div class="flex items-start justify-between gap-3"> <div class="flex items-center gap-3.5 min-w-0"> <div class="w-10 h-10 rounded-xl hl-muted border hl-border p-1.5 flex items-center justify-center shrink-0"> <img :src="card.iconUrl" :alt="card.title" class="w-full h-full object-contain" /> </div> <div class="min-w-0"> <div class="flex items-center gap-2"> <h3 class="text-sm font-semibold truncate group-hover:underline">{{ card.title }}</h3> <span class="w-2 h-2 rounded-full shrink-0" :style="{ backgroundColor: card.online !== false ? 'var(--success)' : '#ef4444' }" ></span> </div> <p class="text-[11px] font-mono-num truncate mt-0.5" style="color: var(--text-muted)"> {{ card.subtitle }} </p> </div> </div> <!-- Action Pill or External Arrow --> <button v-if="card.actionType === 'drawer'" @click.prevent="showAgyModal = true" class="px-2 py-1 rounded-md text-[11px] font-medium shrink-0 transition-opacity hover:opacity-80" style="background-color: var(--accent-soft); color: var(--accent);" > 页内终端 </button> <button v-else-if="card.actionType === 'backup'" @click.prevent="triggerAction('backup_nas', '正在打包备份至极空间 Z4...')" class="px-2 py-1 rounded-md text-[11px] font-medium shrink-0 transition-opacity hover:opacity-80" style="background-color: var(--success-soft); color: var(--success);" > 立即备份 </button> <span v-else class="text-xs font-mono-num px-2 py-0.5 rounded hl-muted shrink-0" style="color: var(--text-muted)"> {{ resolveBadge(card) }} </span> </div> <p class="text-xs leading-relaxed line-clamp-2" style="color: var(--text-muted)"> {{ card.desc }} </p> </div> <!-- Card Footer: gethomepage.dev Signature 3-Cell Inline Telemetry Widget --> <div class="hl-widget-footer grid grid-cols-3 divide-x hl-border text-center"> <div v-for="(stat, idx) in card.stats" :key="idx" class="py-2.5 px-2 flex flex-col items-center justify-center" > <span class="text-[10px] uppercase tracking-wider truncate max-w-full" style="color: var(--text-subtle)"> {{ stat.label }} </span> <span class="text-xs font-semibold font-mono-num mt-0.5 truncate max-w-full" :style="stat.highlight ? 'color: var(--success)' : 'color: var(--text-main)'" > {{ stat.value }} </span> </div> </div> </a> </div> </section> <!-- ===================================================================== 3. Bottom Infrastructure & Quick Bookmarks / SSH Shelf ===================================================================== --> <section class="space-y-3 pt-1"> <div class="flex items-center justify-between px-0.5"> <div class="flex items-center gap-2"> <h2 class="text-xs font-semibold uppercase tracking-wider" style="color: var(--text-muted)"> 终端快捷指令与备用直连书签 </h2> <span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">· 点击指令即复制</span> </div> <div class="flex items-center gap-3 text-xs"> <button @click="triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...')" class="hover:underline font-medium" style="color: var(--accent)" > 刷新 DDNS 解析 </button> <button @click="triggerAction('mount_nas', '正在巡检极空间 20TB SMB 挂载...')" class="hover:underline font-medium" style="color: var(--accent)" > 巡检极空间挂载 </button> </div> </div> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3"> <button v-for="item in sshList" :key="item.cmd" @click="copyCmd(item.cmd, item.label)" class="hl-card hl-card-interactive px-4 py-3 text-left flex items-center justify-between gap-2" > <div class="min-w-0"> <div class="text-xs font-semibold">{{ item.label }}</div> <div class="text-[11px] font-mono-num truncate mt-0.5" style="color: var(--accent)">{{ item.cmd }}</div> </div> <span class="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted shrink-0" style="color: var(--text-muted)"> COPY </span> </button> </div> </section> </main> <!-- ======================================================================= 4. Global Cmd+K Command Palette (Raycast / Shadcn Command Dialog) ======================================================================= --> <div v-if="showCmdPalette" @click.self="showCmdPalette = false" class="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-start justify-center pt-20 px-4" > <div class="w-full max-w-xl hl-card overflow-hidden shadow-2xl"> <div class="px-4 py-3 border-b hl-border flex items-center gap-3"> <i data-lucide="search" class="w-4 h-4" style="color: var(--text-muted)"></i> <input ref="cmdInputRef" v-model="cmdQuery" type="text" placeholder="输入服务名称、SSH 指令、路由模式或主题..." class="w-full bg-transparent text-sm outline-none" @keydown.esc="showCmdPalette = false" @keydown.enter="runFirstCommand" /> <kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">ESC</kbd> </div> <div class="max-h-80 overflow-y-auto p-2 divide-y hl-border text-xs"> <div class="py-1 space-y-1"> <div class="px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider" style="color: var(--text-subtle)"> 快速操作与服务直达 </div> <button v-for="cmd in filteredCommands" :key="cmd.id" @click="executeCommand(cmd)" class="w-full px-3 py-2 rounded-lg text-left flex items-center justify-between hover:opacity-90 transition-colors" style="background-color: transparent;" onmouseover="this.style.backgroundColor='var(--bg-muted)'" onmouseout="this.style.backgroundColor='transparent'" > <div class="flex items-center gap-2.5"> <span class="font-medium">{{ cmd.title }}</span> <span class="font-mono-num text-[11px]" style="color: var(--text-muted)">{{ cmd.hint }}</span> </div> <span class="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted" style="color: var(--text-muted)"> {{ cmd.type }} </span> </button> </div> </div> </div> </div> <!-- ======================================================================= 5. Embedded AGY Cloud Terminal Modal (tmux persistent workbench) ======================================================================= --> <div v-if="showAgyModal" class="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"> <div class="w-full h-full max-w-6xl hl-card flex flex-col overflow-hidden shadow-2xl"> <div class="px-4 py-2.5 border-b hl-border flex items-center justify-between hl-muted"> <div class="flex items-center gap-2.5"> <img src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png" class="w-4 h-4" /> <span class="text-xs font-mono-num font-medium">AGY 云端工作台 · tmux 持久会话 (关窗不断连)</span> </div> <div class="flex items-center gap-2"> <a :href="agyModalUrl" target="_blank" class="hl-btn-outline px-2.5 py-1 text-xs font-mono-num"> 新标签页全屏 ↗ </a> <button @click="showAgyModal = false" class="hl-btn-primary px-3 py-1 text-xs"> 收起终端 </button> </div> </div> <iframe :src="agyModalUrl" class="w-full flex-1 bg-black border-0"></iframe> </div> </div> </div> <script> const { createApp, ref, computed, onMounted, nextTick } = Vue; createApp({ setup() { const currentTheme = ref(localStorage.getItem('hl_theme') || 'zinc-dark'); const routeMode = ref(localStorage.getItem('hl_route_mode') || 'auto'); const showAgyModal = ref(false); const showCmdPalette = ref(false); const cmdQuery = ref(''); const cmdInputRef = ref(null); const toast = ref(''); const actionLoading = ref(false); const themes = [ { id: 'zinc-dark', label: '深色极客', dot: '#18181b' }, { id: 'zinc-light', label: '浅色纯净', dot: '#f8fafc' }, { id: 'catppuccin', label: 'Catppuccin', dot: '#cba6f7' }, { id: 'nord', label: 'Nord', dot: '#88c0d0' }, ]; const modes = [ { id: 'auto', label: '智能路由' }, { id: 'edge443', label: '443 免端口' }, { id: 'home10443', label: '10443 家宽' }, { id: 'lan', label: 'LAN 局域网' }, ]; const setTheme = (id) => { currentTheme.value = id; localStorage.setItem('hl_theme', id); document.documentElement.setAttribute('data-theme', id); }; const telemetry = ref({ mini: { chip: 'Apple M6 (12核)', ip: '192.168.31.164', tailscale_ip: '100.108.205.40', cpu_pct: 6.5, load_1m: 0.85, mem_used_gb: 11.8, mem_total_gb: 24.0, mem_pct: 49.2, disk_used_gb: 59.9, disk_total_gb: 460.4, disk_pct: 13.0, uptime: '运行中', containers: [{ name: 'vaultwarden' }], agy_tmux: true, }, nas: { ip: '192.168.31.185', online: true, mounted: true, total_tb: 19.94, used_tb: 3.36, free_tb: 16.58, pct: 16.8, latest_backup: '11-06 23:17', }, network: { home_wan_ip: '124.90.142.21', ai_policy: 'Ai平台 → 狮城节点', sg_node: '233boy-sea', sg_egress_ip: '104.43.105.243', sg_delay_ms: 68, }, services: { agy: true, vaultwarden: true, nas: true, clash: true, router: true, uptime: true, } }); const effectiveMode = computed(() => { localStorage.setItem('hl_route_mode', routeMode.value); if (routeMode.value !== 'auto') return routeMode.value; const host = window.location.hostname; if (host.startsWith('192.168.') || host.startsWith('100.') || host === 'localhost') { return 'lan'; } if (host.includes('.home.961504.xyz') || window.location.port === '10443') { return 'home10443'; } return 'edge443'; }); const activeModeLabel = computed(() => { const m = effectiveMode.value; if (m === 'lan') return 'LAN 局域网极速直连 (192.168.31.x)'; if (m === 'home10443') return '10443 家宽公网 HTTPS 直连'; return '443 免端口 HTTPS 直达 (*.961504.xyz)'; }); // Categorized Service Rows with Homepage-style Inline Telemetry Widgets const serviceGroups = computed(() => { const t = telemetry.value; 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 }, ] }, ] } ]; }); const sshList = [ { label: 'SSH 连接 Mac mini', cmd: 'ssh mini' }, { label: '终端接力 AGY 会话', cmd: 'ssh mini -t "tmux attach -t agy"' }, { label: 'SSH 连接新加坡 Azure', cmd: 'ssh azure' }, { label: 'SSH 连接小米主路由', cmd: 'ssh root@192.168.31.1' }, ]; const resolveUrl = (card) => { const m = effectiveMode.value; if (m === 'lan') return card.lanUrl; if (m === 'home10443') return card.homeUrl; return card.edgeUrl; }; const resolveBadge = (card) => { const m = effectiveMode.value; if (m === 'lan') return 'LAN 直连'; if (m === 'home10443') return '10443'; return '443 免端口'; }; const agyModalUrl = computed(() => { const card = serviceGroups.value[0].items[0]; return resolveUrl(card); }); const showToast = (msg) => { toast.value = msg; setTimeout(() => { if (toast.value === msg) toast.value = ''; }, 3000); }; const copyCmd = async (cmd, label) => { try { await navigator.clipboard.writeText(cmd); showToast(`已复制「${label}」: ${cmd}`); } catch { showToast(cmd); } }; const fetchTelemetry = async () => { try { const res = await fetch('/api/telemetry'); if (res.ok) { telemetry.value = await res.json(); } } catch (e) {} }; const triggerAction = async (action, msg) => { actionLoading.value = true; showToast(msg); try { const res = await fetch(`/api/action/${action}`, { method: 'POST' }); const data = await res.json(); showToast(data.message || '操作已完成'); await fetchTelemetry(); } catch { showToast('请求失败'); } finally { actionLoading.value = false; } }; const allCommands = computed(() => [ { id: 'open-agy-drawer', title: '在页内唤起 AGY 云端终端', hint: 'tmux attach -t agy', type: '终端', run: () => { showAgyModal.value = true; } }, { id: 'open-agy', title: '新窗口打开 AGY 云端工作台', hint: 'https://agy.961504.xyz', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[0]), '_blank') }, { id: 'open-z4', title: '打开 极空间 Z4 私有云 (20TB)', hint: '192.168.31.185:5055', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[1]), '_blank') }, { id: 'open-vw', title: '打开 Vaultwarden 密码保险箱', hint: 'https://vw.961504.xyz', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[2]), '_blank') }, { id: 'open-clash', title: '打开 Mihomo 路由分流面板', hint: '192.168.31.1:9999', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[1].items[0]), '_blank') }, { id: 'act-backup', title: '立即打包备份核心数据至极空间 Z4', hint: 'backup-to-nas.sh', type: '运维', run: () => triggerAction('backup_nas', '正在打包备份至极空间 Z4...') }, { id: 'act-ddns', title: '立即刷新 Cloudflare DDNS 解析', hint: 'cloudflare-ddns.sh', type: '运维', run: () => triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...') }, { id: 'ssh-mini', title: '复制 SSH 连接 Mac mini 指令', hint: 'ssh mini', type: 'SSH', run: () => copyCmd('ssh mini', 'SSH Mac mini') }, { id: 'ssh-agy', title: '复制 终端接力 AGY 会话指令', hint: 'ssh mini -t "tmux attach -t agy"', type: 'SSH', run: () => copyCmd('ssh mini -t "tmux attach -t agy"', '接力 AGY') }, { id: 'theme-light', title: '切换主题: 浅色纯净 (Shadcn Light)', hint: 'zinc-light', type: '主题', run: () => setTheme('zinc-light') }, { id: 'theme-dark', title: '切换主题: 深色极客 (Shadcn Dark)', hint: 'zinc-dark', type: '主题', run: () => setTheme('zinc-dark') }, { id: 'theme-cat', title: '切换主题: Catppuccin Mocha', hint: 'catppuccin', type: '主题', run: () => setTheme('catppuccin') }, { id: 'theme-nord', title: '切换主题: Nord 极地霜蓝', hint: 'nord', type: '主题', run: () => setTheme('nord') }, ]); const filteredCommands = computed(() => { const q = cmdQuery.value.trim().toLowerCase(); if (!q) return allCommands.value; return allCommands.value.filter(c => c.title.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q)); }); const openCmdPalette = () => { cmdQuery.value = ''; showCmdPalette.value = true; nextTick(() => { if (cmdInputRef.value) cmdInputRef.value.focus(); }); }; const executeCommand = (cmd) => { showCmdPalette.value = false; cmd.run(); }; const runFirstCommand = () => { if (filteredCommands.value.length > 0) { executeCommand(filteredCommands.value[0]); } }; onMounted(() => { setTheme(currentTheme.value); nextTick(() => { if (window.lucide) window.lucide.createIcons(); }); fetchTelemetry(); setInterval(fetchTelemetry, 3000); window.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); openCmdPalette(); } }); }); return { themes, currentTheme, setTheme, modes, routeMode, activeModeLabel, telemetry, serviceGroups, sshList, showAgyModal, agyModalUrl, showCmdPalette, cmdQuery, cmdInputRef, filteredCommands, openCmdPalette, executeCommand, runFirstCommand, toast, actionLoading, resolveUrl, resolveBadge, copyCmd, triggerAction, }; } }).mount('#app'); </script> </body> </html>
Commit Changes
Add a Signed-off-by trailer by the committer at the end of the commit log message.
Commit directly to the
main
branch.
Create a
new branch
for this commit and start a pull request.
Commit Changes
Cancel