<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>项目服务管理面板 v3.3.1</title>
    <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
    <style>
        body { background: #0f172a; }
        .card { background: #1e293b; border: 1px solid #334155; }
        .status-dot { width: 12px; height: 12px; border-radius: 50%; }
        .status-running { background: #22c55e; box-shadow: 0 0 10px #22c55e; }
        .status-stopped { background: #ef4444; }
        .status-partial { background: #f59e0b; }
        .status-ready { background: #3b82f6; }
        .status-configured { background: #22c55e; }
        .status-not-configured { background: #6b7280; }
        .status-completed { background: #8b5cf6; }
        .btn { transition: all 0.2s; }
        .btn:hover { transform: translateY(-1px); }
        .btn:active { transform: translateY(0); }
        .type-badge { font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; }
        .nav-buttons { position: fixed; right: 20px; bottom: 20px; z-index: 100; display: flex; flex-direction: column; gap: 8px; }
        .nav-btn { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; background: #334155; border: 1px solid #475569; }
        .nav-btn:hover { background: #475569; transform: scale(1.1); }
        .nav-btn i { font-size: 20px; color: #94a3b8; }
        .nav-btn:hover i { color: #f1f5f9; }
        .filter-bar { position: sticky; top: 0; z-index: 50; display: flex; gap: 8px; padding: 12px 0; background: #0f172a; margin-bottom: 16px; flex-wrap: wrap; }
        .filter-bar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #94a3b8; font-size: 14px; cursor: pointer; transition: all 0.2s; }
        .filter-bar-btn:hover { background: #334155; color: #f1f5f9; }
        .filter-bar-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
        .filter-bar-btn.add-btn { background: #22c55e; border-color: #22c55e; color: #fff; }
        .filter-bar-btn.add-btn:hover { background: #16a34a; }
        .realtime-toggle { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; }
        .realtime-toggle.active { background: #22c55e/20; border-color: #22c55e; }
        .speed-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; background: #334155; font-size: 13px; }
        .speed-badge.upload { color: #f97316; }
        .speed-badge.download { color: #3b82f6; }
        .threshold-section { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
        .threshold-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #334155; }
        .threshold-item:last-child { border-bottom: none; }
        .notification-toggle { display: flex; align-items: center; gap: 8px; padding: 12px; background: #334155; border-radius: 8px; margin-top: 12px; }
        .notification-toggle label { cursor: pointer; }
        .threshold-label { flex: 1; display: flex; align-items: center; gap: 8px; }
        .threshold-input { width: 60px; background: #334155; border: 1px solid #475569; border-radius: 6px; padding: 4px 8px; color: #f1f5f9; text-align: center; }
        .threshold-input:focus { outline: none; border-color: #3b82f6; }
        .threshold-icon { font-size: 18px; }
        .threshold-warning { color: #f97316; }
        .alert-popup { position: fixed; top: 20px; right: 20px; z-index: 1000; background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); border-radius: 12px; padding: 16px 20px; color: white; box-shadow: 0 10px 40px rgba(0,0,0,0.3); animation: slideIn 0.3s ease-out; max-width: 320px; }
        .alert-popup .alert-title { font-weight: bold; font-size: 16px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
        .alert-popup .alert-content { font-size: 14px; }
        .alert-popup .alert-close { position: absolute; top: 8px; right: 12px; cursor: pointer; opacity: 0.7; }
        .alert-popup .alert-close:hover { opacity: 1; }
        @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
        @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
        .tab-btn { border-bottom: 2px solid transparent; }
        .tab-btn.active { border-bottom-color: #3b82f6; }
        .cron-card { transition: all 0.2s; }
        .cron-card:hover { border-color: #475569; }
        .history-item { border-left: 3px solid #334155; padding-left: 12px; }
        .history-item.rollback { border-left-color: #f59e0b; }
        .history-item.update { border-left-color: #3b82f6; }
        .history-item.create { border-left-color: #22c55e; }
        .history-item.delete { border-left-color: #ef4444; }
        .history-item.toggle { border-left-color: #8b5cf6; }
        .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; }
        .modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; }
        .section-header { cursor: pointer; user-select: none; }
        .section-header:hover { opacity: 0.85; }
        .chevron { display: inline-block; transition: transform 0.2s ease; font-size: 14px; }
    </style>
</head>
<body class="min-h-screen text-gray-100">
    <div class="container mx-auto px-4 py-8">
        <!-- 头部 -->
        <div class="flex items-center justify-between mb-6">
            <div>
                <h1 class="text-3xl font-bold flex items-center gap-3">
                    <i class="ri-dashboard-3-line text-blue-400"></i>
                    项目服务管理面板
                    <span class="text-sm text-gray-500">v2.1</span>
                </h1>
            </div>
            <div class="flex items-center gap-3">
                <div class="flex items-center gap-2">
                    <span class="text-gray-400 text-sm">对外IP:</span>
                    <input type="text" id="externalIp" value="121.40.164.32" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-sm w-28" onchange="saveExternalIp()">
                </div>
                <button onclick="refreshAll()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
                    <i class="ri-refresh-line"></i> 刷新
                </button>
                <button onclick="exportConfig()" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg flex items-center gap-2">
                    <i class="ri-download-line"></i> 导出配置
                </button>
                <label class="btn bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg flex items-center gap-2 cursor-pointer">
                    <i class="ri-upload-line"></i> 导入配置
                    <input type="file" accept=".json" onchange="importConfig(event)" class="hidden">
                </label>
                <div class="flex items-center gap-1">
                    <span class="text-gray-400 text-xs">间隔:</span>
                    <input type="number" id="refreshInterval" value="30" min="5" max="300" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs w-12" onchange="updateRefreshInterval()">
                    <span class="text-gray-500 text-xs">s</span>
                </div>
                <span id="updateTime" class="text-gray-400 text-sm"></span>
                <div id="connectionStatus" class="flex items-center gap-1 px-2 py-1 rounded bg-green-500/20">
                    <div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
                    <span class="text-green-400 text-xs">已连接</span>
                </div>
            </div>
        </div>

        <!-- Tab 切换 -->
        <div class="flex gap-4 mb-6 border-b border-gray-700">
            <button onclick="switchTab('projects')" class="tab-btn active px-4 py-2 text-gray-300 hover:text-white" data-tab="projects">
                <i class="ri-apps-line"></i> 项目服务
            </button>
            <button onclick="switchTab('system')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="system">
                <i class="ri-dashboard-2-line"></i> 系统资源
            </button>
            <button onclick="switchTab('cron')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="cron">
                <i class="ri-timer-line"></i> Cron 管理
            </button>
            <button onclick="switchTab('docker')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="docker">
                <i class="ri-ship-line"></i> Docker 监控
            </button>
        </div>

        <!-- 项目服务 Tab -->
        <div id="projectsTab">
            <!-- 统计卡片 -->
            <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">Web服务</p>
                            <p id="webCount" class="text-2xl font-bold text-blue-400">-</p>
                        </div>
                        <i class="ri-server-line text-3xl text-blue-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">运行中</p>
                            <p id="runningCount" class="text-2xl font-bold text-green-400">-</p>
                        </div>
                        <i class="ri-play-circle-line text-3xl text-green-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">Cron任务</p>
                            <p id="cronCount" class="text-2xl font-bold text-yellow-400">-</p>
                        </div>
                        <i class="ri-time-line text-3xl text-yellow-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">CLI工具</p>
                            <p id="cliCount" class="text-2xl font-bold text-purple-400">-</p>
                        </div>
                        <i class="ri-terminal-box-line text-3xl text-purple-400 opacity-50"></i>
                    </div>
                </div>
            </div>

            <!-- 筛选器（顶部固定） -->
            <div class="filter-bar">
                <button onclick="filterType('all')" class="filter-bar-btn active" data-type="all"><i class="ri-apps-line"></i> 全部</button>
                <button onclick="filterType('web')" class="filter-bar-btn" data-type="web"><i class="ri-server-line"></i> Web服务</button>
                <button onclick="filterType('custom')" class="filter-bar-btn" data-type="custom"><i class="ri-global-line"></i> 自定义</button>
                <button onclick="filterType('cli')" class="filter-bar-btn" data-type="cli"><i class="ri-terminal-box-line"></i> CLI工具</button>
                <button onclick="filterType('extension')" class="filter-bar-btn" data-type="extension"><i class="ri-chrome-line"></i> 插件</button>
                <button onclick="showAddServiceModal()" class="filter-bar-btn add-btn"><i class="ri-add-circle-line"></i> 新增服务</button>
            </div>

            <!-- 项目列表 -->
            <div id="projectsList" class="space-y-4">
                <div class="text-center py-12 text-gray-400"><i class="ri-loader-4-line text-4xl animate-spin"></i><p class="mt-2">加载中...</p></div>
            </div>
            
            <!-- 发现新服务 -->
            <div id="discoverySection" class="hidden mt-4 pt-4 border-t border-gray-600">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="text-gray-200 text-sm font-semibold flex items-center gap-2">
                        <i class="ri-radar-line text-cyan-400"></i> 发现未注册服务
                    </h3>
                    <div class="flex items-center gap-2">
                        <span id="discoveryCount" class="text-xs text-gray-500"></span>
                        <button onclick="scanServices()" class="btn bg-cyan-600 hover:bg-cyan-700 px-2 py-1 rounded text-xs flex items-center gap-1">
                            <i class="ri-scan-line"></i> 扫描
                        </button>
                    </div>
                </div>
                <div id="discoveryList" class="space-y-2">
                    <div class="text-gray-500 text-xs text-center py-2">点击「扫描」发现本机未注册的新服务</div>
                </div>
            </div>
        </div>

        <!-- 系统资源 Tab -->
        <div id="systemTab" class="hidden">
            <!-- 系统概览 -->
            <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">系统运行时间</p>
                            <p id="sysUptime" class="text-lg font-bold text-green-400">-</p>
                        </div>
                        <i class="ri-time-line text-3xl text-green-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">启动时间</p>
                            <p id="sysBootTime" class="text-lg font-bold text-blue-400">-</p>
                        </div>
                        <i class="ri-calendar-line text-3xl text-blue-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">进程数</p>
                            <p id="sysProcessCount" class="text-lg font-bold text-yellow-400">-</p>
                        </div>
                        <i class="ri-terminal-line text-3xl text-yellow-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">CPU核心</p>
                            <p id="sysCpuCount" class="text-lg font-bold text-purple-400">-</p>
                        </div>
                        <i class="ri-cpu-line text-3xl text-purple-400 opacity-50"></i>
                    </div>
                </div>
            </div>

            <!-- CPU & 内存 -->
            <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
                <div class="card rounded-xl p-6">
                    <div class="flex items-center justify-between mb-4">
                        <h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-cpu-line text-blue-400"></i> CPU 使用率</h3>
                        <span id="cpuPercent" class="text-2xl font-bold text-blue-400">-</span>
                    </div>
                    <div class="w-full bg-gray-700 rounded-full h-4 mb-2">
                        <div id="cpuBar" class="bg-blue-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
                    </div>
                    <p class="text-gray-400 text-sm">物理核心: <span id="cpuPhysical">-</span> | 逻辑核心: <span id="cpuLogical">-</span></p>
                    <div class="mt-3 pt-3 border-t border-gray-700">
                        <p class="text-gray-400 text-sm flex items-center gap-4">
                            <span><i class="ri-temp-hot-line text-red-400"></i> CPU0: <span id="cpuTemp0" class="text-white font-medium">-</span>°C</span>
                            <span><i class="ri-temp-hot-line text-red-400"></i> CPU1: <span id="cpuTemp1" class="text-white font-medium">-</span>°C</span>
                        </p>
                    </div>
                </div>
                <div class="card rounded-xl p-6">
                    <div class="flex items-center justify-between mb-4">
                        <h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-database-2-line text-green-400"></i> 内存使用</h3>
                        <span id="memPercent" class="text-2xl font-bold text-green-400">-</span>
                    </div>
                    <div class="w-full bg-gray-700 rounded-full h-4 mb-2">
                        <div id="memBar" class="bg-green-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
                    </div>
                    <p class="text-gray-400 text-sm">已用: <span id="memUsed">-</span> GB / 总量: <span id="memTotal">-</span> GB | 可用: <span id="memAvailable">-</span> GB</p>
                </div>
            </div>

            <!-- 磁盘 & 网络 -->
            <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
                <div class="card rounded-xl p-6">
                    <div class="flex items-center justify-between mb-4">
                        <h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-hard-drive-2-line text-orange-400"></i> 磁盘使用</h3>
                        <span id="diskPercent" class="text-2xl font-bold text-orange-400">-</span>
                    </div>
                    <div class="w-full bg-gray-700 rounded-full h-4 mb-2">
                        <div id="diskBar" class="bg-orange-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
                    </div>
                    <p class="text-gray-400 text-sm">已用: <span id="diskUsed">-</span> GB / 总量: <span id="diskTotal">-</span> GB | 剩余: <span id="diskFree">-</span> GB</p>
                </div>
                <div class="card rounded-xl p-6">
                    <div class="flex items-center justify-between mb-4">
                        <h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-wifi-line text-cyan-400"></i> 网络流量</h3>
                    </div>
                    <!-- 实时网速 -->
                    <div class="flex gap-3 mb-4">
                        <span class="speed-badge upload"><i class="ri-upload-line"></i> ↑ <span id="netSentSpeed">-</span></span>
                        <span class="speed-badge download"><i class="ri-download-line"></i> ↓ <span id="netRecvSpeed">-</span></span>
                    </div>
                    <div class="grid grid-cols-2 gap-4">
                        <div>
                            <p class="text-gray-400 text-sm mb-1">累计发送</p>
                            <p id="netSent" class="text-xl font-bold text-cyan-400">-</p>
                            <p class="text-gray-500 text-xs">MB</p>
                        </div>
                        <div>
                            <p class="text-gray-400 text-sm mb-1">累计接收</p>
                            <p id="netRecv" class="text-xl font-bold text-purple-400">-</p>
                            <p class="text-gray-500 text-xs">MB</p>
                        </div>
                    </div>
                </div>
            </div>

            <!-- 阈值监控设置 -->
            <div class="threshold-section">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="font-bold text-gray-200 flex items-center gap-2">
                        <i class="ri-alarm-warning-line text-orange-400"></i> 监控阈值设置
                    </h3>
                    <span class="text-xs text-gray-500">超过阈值将弹窗警告</span>
                </div>
                <div class="threshold-item">
                    <div class="threshold-label">
                        <i class="ri-cpu-line threshold-icon text-blue-400"></i>
                        <span class="text-gray-300 text-sm">CPU 使用率</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <input type="number" id="thresholdCpu" class="threshold-input" min="0" max="100" value="80" onchange="saveThresholds()">
                        <span class="text-gray-500 text-xs">%</span>
                    </div>
                </div>
                <div class="threshold-item">
                    <div class="threshold-label">
                        <i class="ri-temp-hot-line threshold-icon text-red-400"></i>
                        <span class="text-gray-300 text-sm">CPU 温度</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <input type="number" id="thresholdCpuTemp" class="threshold-input" min="30" max="120" value="80" onchange="saveThresholds()">
                        <span class="text-gray-500 text-xs">°C</span>
                    </div>
                </div>
                <div class="threshold-item">
                    <div class="threshold-label">
                        <i class="ri-database-2-line threshold-icon text-green-400"></i>
                        <span class="text-gray-300 text-sm">内存使用率</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <input type="number" id="thresholdMemory" class="threshold-input" min="0" max="100" value="85" onchange="saveThresholds()">
                        <span class="text-gray-500 text-xs">%</span>
                    </div>
                </div>
                <div class="threshold-item">
                    <div class="threshold-label">
                        <i class="ri-hard-drive-2-line threshold-icon text-orange-400"></i>
                        <span class="text-gray-300 text-sm">磁盘使用率</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <input type="number" id="thresholdDisk" class="threshold-input" min="0" max="100" value="90" onchange="saveThresholds()">
                        <span class="text-gray-500 text-xs">%</span>
                    </div>
                </div>
                <div class="threshold-item">
                    <div class="threshold-label">
                        <i class="ri-timer-line threshold-icon text-purple-400"></i>
                        <span class="text-gray-300 text-sm">警告间隔</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <input type="number" id="thresholdInterval" class="threshold-input" min="10" max="300" value="60" onchange="saveThresholds()">
                        <span class="text-gray-500 text-xs">秒</span>
                    </div>
                </div>
                <!-- 桌面通知开关 -->
                <div class="notification-toggle">
                    <input type="checkbox" id="enableDesktopNotify" onchange="toggleDesktopNotify()" class="w-4 h-4 cursor-pointer">
                    <label for="enableDesktopNotify" class="text-gray-300 text-sm cursor-pointer">
                        <i class="ri-notification-line text-yellow-400"></i> 系统桌面通知
                    </label>
                    <span id="notifyStatus" class="text-xs text-gray-500 ml-1"></span>
                </div>

                <!-- 邮件通知规则 -->
                <div class="mt-4 pt-4 border-t border-gray-600">
                    <div class="flex items-center justify-between mb-3">
                        <h4 class="text-gray-200 text-sm font-medium flex items-center gap-2">
                            <i class="ri-mail-line text-green-400"></i> 邮件通知规则
                        </h4>
                        <div class="flex items-center gap-2">
                            <button onclick="showTestEmailModal()" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-xs flex items-center gap-1">
                                <i class="ri-send-plane-line"></i> 测试发送
                            </button>
                            <button onclick="showAddEmailRuleModal()" class="btn bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs flex items-center gap-1">
                                <i class="ri-add-line"></i> 添加规则
                            </button>
                        </div>
                    </div>
                    <div id="emailRulesList" class="space-y-2 text-sm">
                        <div class="text-gray-500 text-center py-4">暂无邮件通知规则</div>
                    </div>
                </div>
            </div>

            <!-- 控制按钮 -->
            <div class="flex gap-3 mb-4 items-center">
                <button onclick="loadSystemStats()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
                    <i class="ri-refresh-line"></i> 刷新数据
                </button>
                <div class="realtime-toggle" id="realtimeToggle">
                    <input type="checkbox" id="realtimeCheck" onchange="toggleRealtime()" class="w-4 h-4 cursor-pointer">
                    <label for="realtimeCheck" class="text-gray-300 text-sm cursor-pointer">实时监控</label>
                    <select id="realtimeIntervalSelect" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs ml-2 cursor-pointer" onchange="updateRealtimeInterval()">
                        <option value="1">1秒</option>
                        <option value="2" selected>2秒</option>
                        <option value="3">3秒</option>
                        <option value="5">5秒</option>
                        <option value="10">10秒</option>
                    </select>
                    <span id="realtimeIndicator" class="text-xs text-green-400 ml-1 hidden">●</span>
                </div>
                <div class="flex items-center gap-2 ml-4">
                    <input type="checkbox" id="showProcessListCheck" class="w-4 h-4 cursor-pointer" onchange="toggleProcessList()">
                    <label for="showProcessListCheck" class="text-gray-300 text-xs cursor-pointer">显示进程列表</label>
                </div>
            </div>

            <!-- 进程列表（实时监控时显示） -->
            <div id="processListSection" class="hidden mt-6">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="font-bold text-gray-200 flex items-center gap-2">
                        <i class="ri-bar-chart-horizontal text-purple-400"></i> CPU占用最高的进程
                    </h3>
                    <div class="flex items-center gap-2">
                        <span class="text-gray-400 text-xs">显示数量:</span>
                        <select id="processLimitSelect" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs" onchange="loadTopProcesses()">
                            <option value="5">5</option>
                            <option value="10" selected>10</option>
                            <option value="15">15</option>
                            <option value="20">20</option>
                            <option value="30">30</option>
                        </select>
                    </div>
                </div>
                <div id="processList" class="space-y-2">
                    <div class="text-gray-500 text-center py-4"><i class="ri-loader-4-line animate-spin"></i> 加载中...</div>
                </div>
            </div>
        </div>

        <!-- Cron 管理 Tab -->
        <div id="cronTab" class="hidden">
            <!-- Cron 统计 -->
            <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">总任务数</p>
                            <p id="cronTotal" class="text-2xl font-bold text-orange-400">-</p>
                        </div>
                        <i class="ri-timer-line text-3xl text-orange-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">启用</p>
                            <p id="cronEnabled" class="text-2xl font-bold text-green-400">-</p>
                        </div>
                        <i class="ri-play-circle-line text-3xl text-green-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">禁用</p>
                            <p id="cronDisabled" class="text-2xl font-bold text-red-400">-</p>
                        </div>
                        <i class="ri-pause-circle-line text-3xl text-red-400 opacity-50"></i>
                    </div>
                </div>
                <div class="card rounded-xl p-4">
                    <div class="flex items-center justify-between">
                        <div>
                            <p class="text-gray-400 text-sm">历史记录</p>
                            <p id="cronHistoryCount" class="text-2xl font-bold text-purple-400">-</p>
                        </div>
                        <i class="ri-history-line text-3xl text-purple-400 opacity-50"></i>
                    </div>
                </div>
            </div>

            <!-- Cron 操作按钮 -->
            <div class="flex gap-2 mb-4">
                <button onclick="showAddCronModal()" class="btn bg-green-600 hover:bg-green-700 px-3 py-2 rounded-lg flex items-center gap-1">
                    <i class="ri-add-line"></i> 添加任务
                </button>
                <button onclick="syncFromSystem()" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded-lg flex items-center gap-1">
                    <i class="ri-refresh-line"></i> 从系统同步
                </button>
                <button onclick="loadCronTasks()" class="btn bg-gray-600 hover:bg-gray-700 px-3 py-2 rounded-lg flex items-center gap-1">
                    <i class="ri-reload-line"></i> 刷新列表
                </button>
                <div class="flex items-center gap-2 ml-4">
                    <span class="text-gray-400 text-sm">执行日志:</span>
                    <input type="checkbox" id="enableLogs" onchange="toggleLogs()" class="w-4 h-4">
                </div>
            </div>

            <!-- Cron 任务列表 -->
            <div id="cronTasksList" class="space-y-3">
                <div class="text-center py-12 text-gray-400"><i class="ri-loader-4-line text-4xl animate-spin"></i><p class="mt-2">加载中...</p></div>
            </div>
        </div>

        <!-- Docker 监控 Tab -->
        <div id="dockerTab" class="hidden">
            <!-- Docker 统计卡片 -->
            <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">总容器数</p>
                    <p id="dockerContainersTotal" class="text-xl font-bold text-blue-400">-</p>
                </div>
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">运行中</p>
                    <p id="dockerContainersRunning" class="text-xl font-bold text-green-400">-</p>
                </div>
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">已停止</p>
                    <p id="dockerContainersStopped" class="text-xl font-bold text-red-400">-</p>
                </div>
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">总镜像数</p>
                    <p id="dockerImagesTotal" class="text-xl font-bold text-purple-400">-</p>
                </div>
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">磁盘占用</p>
                    <p id="dockerDiskSize" class="text-xl font-bold text-yellow-400">-</p>
                </div>
                <div class="card rounded-xl p-3">
                    <p class="text-gray-400 text-xs">可回收</p>
                    <p id="dockerReclaimable" class="text-xl font-bold text-orange-400">-</p>
                </div>
            </div>

            <!-- 操作按钮 -->
            <div class="flex gap-2 mb-4 flex-wrap">
                <button onclick="loadDockerData()" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded-lg text-sm flex items-center gap-1">
                    <i class="ri-refresh-line"></i> 刷新
                </button>
                <button onclick="dockerPrune('containers')" class="btn bg-yellow-600 hover:bg-yellow-700 px-3 py-2 rounded-lg text-sm flex items-center gap-1">
                    <i class="ri-delete-bin-line"></i> 清理容器
                </button>
                <button onclick="dockerPrune('images')" class="btn bg-orange-600 hover:bg-orange-700 px-3 py-2 rounded-lg text-sm flex items-center gap-1">
                    <i class="ri-image-line"></i> 清理镜像
                </button>
                <button onclick="dockerPrune('all')" class="btn bg-red-600 hover:bg-red-700 px-3 py-2 rounded-lg text-sm flex items-center gap-1">
                    <i class="ri-broom-line"></i> 全部清理
                </button>
                <span class="text-gray-500 text-xs flex items-center ml-2">版本: <span id="dockerVersion" class="text-gray-300 ml-1">-</span></span>
            </div>

            <!-- 容器列表 -->
            <h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-containers-grid', this.querySelector('.chevron'))">
                <i class="ri-arrow-down-s-line chevron"></i><i class="ri-computer-line text-blue-400"></i> 容器列表
                <span id="dockerContainerCount" class="text-xs text-gray-500"></span>
            </h3>
            <div id="docker-containers-grid" class="space-y-2 mb-6">
                <div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
            </div>

            <!-- 镜像列表 -->
            <h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-images-grid', this.querySelector('.chevron'))">
                <i class="ri-arrow-down-s-line chevron"></i><i class="ri-stack-line text-purple-400"></i> 镜像列表
                <span id="dockerImageCount" class="text-xs text-gray-500"></span>
            </h3>
            <div id="docker-images-grid" class="space-y-1">
                <div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
            </div>
        </div>
    </div>

    <!-- 日志模态框 -->
    <div id="logModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
        <div class="card rounded-xl w-11/12 max-w-4xl max-h-[80vh] flex flex-col">
            <div class="flex items-center justify-between p-4 border-b border-gray-700">
                <h3 id="logTitle" class="font-bold text-lg">日志</h3>
                <button onclick="closeLogModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            <div class="flex-1 overflow-auto p-4">
                <pre id="logContent" class="text-sm text-gray-300 font-mono whitespace-pre-wrap"></pre>
            </div>
        </div>
    </div>

    <!-- 添加/编辑 Cron 模态框 -->
    <div id="cronModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6">
            <div class="flex items-center justify-between mb-4">
                <h3 id="cronModalTitle" class="font-bold text-lg">添加 Cron 任务</h3>
                <button onclick="closeCronModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            
            <form id="cronForm" onsubmit="saveCronTask(event)">
                <input type="hidden" id="cronTaskId">
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">任务名称 *</label>
                    <input type="text" id="cronName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：服务监控">
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">时间表达式 *</label>
                    <input type="text" id="cronExpression" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：0 4 * * * (每天凌晨4点)">
                    <p id="cronHumanTime" class="text-sm text-blue-400 mt-1"></p>
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">执行命令 *</label>
                    <textarea id="cronCommand" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" rows="3" placeholder="例如：python3 /path/to/script.py"></textarea>
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">任务分类</label>
                    <select id="cronCategory" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
                        <option value="monitor">系统监控</option>
                        <option value="backup">数据备份</option>
                        <option value="cleanup">清理任务</option>
                        <option value="report">报表生成</option>
                        <option value="sync">数据同步</option>
                        <option value="notification">通知提醒</option>
                        <option value="other">其他任务</option>
                    </select>
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">说明备注</label>
                    <textarea id="cronDescription" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" rows="2" placeholder="任务的详细说明..."></textarea>
                </div>
                
                <div class="flex items-center gap-2 mb-4">
                    <input type="checkbox" id="cronEnabled" checked class="w-4 h-4">
                    <label class="text-gray-300">启用任务</label>
                </div>
                
                <div class="flex justify-end gap-2">
                    <button type="button" onclick="closeCronModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
                    <button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg">保存</button>
                </div>
            </form>
        </div>
    </div>

    <!-- 历史记录模态框 -->
    <div id="historyModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6">
            <div class="flex items-center justify-between mb-4">
                <h3 id="historyModalTitle" class="font-bold text-lg">历史版本</h3>
                <button onclick="closeHistoryModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            <div id="historyList" class="space-y-3"></div>
        </div>
    </div>

    <!-- 自定义链接模态框 -->
    <div id="linkModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6">
            <div class="flex items-center justify-between mb-4">
                <h3 id="linkModalTitle" class="font-bold text-lg">添加自定义链接</h3>
                <button onclick="closeLinkModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            
            <form id="linkForm" onsubmit="saveCustomLink(event)">
                <input type="hidden" id="linkProjectId">
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">链接名称 *</label>
                    <input type="text" id="linkName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：API文档、管理后台">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">端口（使用统一IP）</label>
                    <input type="text" id="linkPort" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：19001" onchange="updateLinkPreview()">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">路径（可选）</label>
                    <input type="text" id="linkPath" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：/admin /api/docs" onchange="updateLinkPreview()">
                    <p id="linkPreview" class="text-sm text-cyan-400 mt-1"></p>
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">完整URL（不填则用端口生成）</label>
                    <input type="text" id="linkFullUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：http://192.168.2.17:19000/docs">
                </div>
                
                <div id="existingLinks" class="mb-4"></div>
                
                <div class="flex justify-end gap-2">
                    <button type="button" onclick="closeLinkModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
                    <button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
                </div>
            </form>
        </div>
    </div>

    <!-- 新增自定义服务模态框 -->
    <div id="addServiceModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6">
            <div class="flex items-center justify-between mb-4">
                <h3 class="font-bold text-lg">新增自定义服务</h3>
                <button onclick="closeAddServiceModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            
            <form id="addServiceForm" onsubmit="saveCustomService(event)">
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">服务名称 *</label>
                    <input type="text" id="serviceName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：外部API、监控面板">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">端口（使用统一IP）</label>
                    <input type="text" id="servicePort" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：8080" onchange="updateServicePreview()">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">路径（可选）</label>
                    <input type="text" id="servicePath" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：/admin" onchange="updateServicePreview()">
                    <p id="servicePreview" class="text-sm text-cyan-400 mt-1"></p>
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">完整URL（不填则用端口生成）</label>
                    <input type="text" id="serviceFullUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：http://external.com:8080">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">后台URL（可选）</label>
                    <input type="text" id="serviceAdminUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：http://external.com:8080/admin">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">描述</label>
                    <input type="text" id="serviceDesc" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="简要描述">
                </div>
                
                <div class="flex justify-end gap-2">
                    <button type="button" onclick="closeAddServiceModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
                    <button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
                </div>
            </form>
        </div>
    </div>

    <!-- 邮件通知规则模态框 -->
    <div id="emailRuleModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6">
            <div class="flex items-center justify-between mb-4">
                <h3 id="emailRuleModalTitle" class="font-bold text-lg">添加邮件通知规则</h3>
                <button onclick="closeEmailRuleModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            
            <form id="emailRuleForm" onsubmit="saveEmailRule(event)">
                <input type="hidden" id="emailRuleId">
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">规则名称 *</label>
                    <input type="text" id="emailRuleName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如：CPU告警通知">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">监控资源 *（可多选组合）</label>
                    <div class="grid grid-cols-2 gap-2">
                        <label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
                            <input type="checkbox" value="cpu" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
                            <i class="ri-cpu-line text-blue-400"></i>
                            <span class="text-gray-200 text-sm">CPU 使用率</span>
                        </label>
                        <label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
                            <input type="checkbox" value="cpu_temp" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
                            <i class="ri-temp-hot-line text-red-400"></i>
                            <span class="text-gray-200 text-sm">CPU 温度</span>
                        </label>
                        <label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
                            <input type="checkbox" value="memory" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
                            <i class="ri-database-2-line text-green-400"></i>
                            <span class="text-gray-200 text-sm">内存使用率</span>
                        </label>
                        <label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
                            <input type="checkbox" value="disk" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
                            <i class="ri-hard-drive-2-line text-orange-400"></i>
                            <span class="text-gray-200 text-sm">磁盘使用率</span>
                        </label>
                        <label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700 col-span-2">
                            <input type="checkbox" value="service_down" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
                            <i class="ri-shut-down-line text-yellow-400"></i>
                            <span class="text-gray-200 text-sm">服务下线监控</span>
                            <span class="text-gray-500 text-xs">检测指定服务长期不在线</span>
                        </label>
                    </div>
                    <p class="text-gray-500 text-xs mt-1">勾选多项可随机组合监控，任一资源超标即触发通知</p>
                </div>
                
                <!-- 服务选择器（仅当勾选「服务下线监控」时显示） -->
                <div class="mb-3" id="serviceDownSelector" style="display:none;">
                    <label class="block text-gray-400 text-sm mb-1">监控服务 *</label>
                    <select id="emailRuleServiceId" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" onchange="onServiceDownChange()">
                        <option value="">-- 请选择要监控的服务 --</option>
                    </select>
                    <p class="text-gray-500 text-xs mt-1">选择需要监控下线状态的服务</p>
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">触发阈值 *</label>
                    <div class="flex items-center gap-2">
                        <input type="number" id="emailRuleThreshold" required min="1" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="80">
                        <span class="text-gray-400" id="emailRuleThresholdUnit">% / °C</span>
                    </div>
                    <p class="text-gray-500 text-xs mt-1" id="emailRuleThresholdHint">对不同资源类型使用统一阈值（如CPU用%则填百分比，温度用°C则填温度值）</p>
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">触发机制 *</label>
                    <select id="emailRuleTriggerType" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" onchange="updateTriggerParams()">
                        <option value="single">一次性触发 - 超过阈值立即通知</option>
                        <option value="continuous">连续触发 - 连续超过阈值N次</option>
                        <option value="duration">持续时间触发 - 累计超过阈值N秒</option>
                        <option value="average">平均值触发 - 最近N次平均值超过阈值</option>
                    </select>
                </div>
                
                <div class="mb-3" id="triggerParamsDiv" style="display:none;">
                    <label class="block text-gray-400 text-sm mb-1" id="triggerParamsLabel">触发参数</label>
                    <div class="flex items-center gap-2">
                        <input type="number" id="emailRuleTriggerParam" min="1" max="100" value="3" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
                        <span class="text-gray-400" id="triggerParamUnit">次</span>
                    </div>
                    <p class="text-gray-500 text-xs mt-1" id="triggerParamHint">连续超过阈值多少次才触发</p>
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">收件邮箱 *</label>
                    <input type="email" id="emailRuleAddress" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="alert@example.com">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">通知间隔</label>
                    <div class="flex items-center gap-2">
                        <input type="number" id="emailRuleInterval" min="60" max="3600" value="300" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
                        <span class="text-gray-400">秒（最少60秒）</span>
                    </div>
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">静默时间区间</label>
                    <p class="text-gray-500 text-xs mb-2">在此时间段内不发送邮件通知（如夜间休息时段）</p>
                    <div class="flex items-center gap-2">
                        <input type="time" id="emailRuleSilentStart" class="bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="23:00">
                        <span class="text-gray-400">至</span>
                        <input type="time" id="emailRuleSilentEnd" class="bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="08:00">
                        <input type="checkbox" id="emailRuleSilentEnabled" class="w-4 h-4 cursor-pointer ml-2">
                        <label for="emailRuleSilentEnabled" class="text-gray-300 text-xs cursor-pointer">启用静默</label>
                    </div>
                </div>
                
                <div class="mb-3 flex items-center gap-2">
                    <input type="checkbox" id="emailRuleEnabled" checked class="w-4 h-4 cursor-pointer">
                    <label for="emailRuleEnabled" class="text-gray-300 text-sm cursor-pointer">启用此规则</label>
                </div>
                
                <div class="flex justify-end gap-2">
                    <button type="button" onclick="closeEmailRuleModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
                    <button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">保存</button>
                </div>
            </form>
        </div>
    </div>

    <!-- 测试邮件发送模态框 -->
    <div id="testEmailModal" class="modal-overlay hidden">
        <div class="card rounded-xl modal-content p-6" style="max-width: 420px;">
            <div class="flex items-center justify-between mb-4">
                <h3 class="font-bold text-lg"><i class="ri-send-plane-line text-blue-400"></i> 测试邮件发送</h3>
                <button onclick="closeTestEmailModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
            </div>
            
            <form id="testEmailForm" onsubmit="sendTestEmail(event)">
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">收件邮箱 *</label>
                    <input type="email" id="testEmailAddress" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="your@email.com">
                </div>
                
                <div class="mb-3">
                    <label class="block text-gray-400 text-sm mb-1">邮件主题</label>
                    <input type="text" id="testEmailSubject" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="[测试邮件] 项目服务管理面板">
                </div>
                
                <div class="mb-4">
                    <label class="block text-gray-400 text-sm mb-1">邮件内容</label>
                    <textarea id="testEmailBody" rows="3" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">这是一封测试邮件，用于验证邮件通知功能是否正常工作。

发送自: 项目服务管理面板
时间: ${new Date().toLocaleString()}</textarea>
                </div>
                
                <div id="testEmailResult" class="mb-3 hidden"></div>
                
                <div class="flex justify-end gap-2">
                    <button type="button" onclick="closeTestEmailModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
                    <button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-1">
                        <i class="ri-send-plane-line"></i> 发送测试
                    </button>
                </div>
            </form>
        </div>
    </div>

    <!-- 导航按钮 -->
    <div class="nav-buttons">
        <button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
        <button onclick="scrollToBottom()" class="nav-btn" title="滚动到底部"><i class="ri-arrow-down-line"></i></button>
    </div>

    <script>
        let projects = [];
        let cronTasks = [];
        let currentTab = 'projects';
        let currentFilter = 'all';
        let externalIp = localStorage.getItem('externalIp') || '192.168.2.17';
        let enableLogs = localStorage.getItem('enableLogs') === 'true';

        document.getElementById('externalIp').value = externalIp;
        document.getElementById('enableLogs').checked = enableLogs;

        // Tab 切换
        // 系统资源监控变量（必须先声明）
        let realtimeTimer = null;
        let systemRefreshTimer = null;
        let realtimeInterval = 2; // 秒
        let lastAlertTime = 0; // 上次警告时间
        let thresholds = { cpu: 80, cpu_temp: 80, memory: 85, disk: 90, interval: 60 };
        let desktopNotifyEnabled = false;

        // ==================== 折叠/展开 ====================
        function getCollapsedState() {
            const stored = localStorage.getItem('collapsedSections');
            return stored ? JSON.parse(stored) : {};
        }
        function saveCollapsedState(state) {
            localStorage.setItem('collapsedSections', JSON.stringify(state));
        }
        function toggleSection(sectionId, chevronEl) {
            const grid = document.getElementById(sectionId);
            if (!grid) return;
            const state = getCollapsedState();
            if (grid.classList.contains('hidden')) {
                grid.classList.remove('hidden');
                if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
                state[sectionId] = false;
            } else {
                grid.classList.add('hidden');
                if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
                state[sectionId] = true;
            }
            saveCollapsedState(state);
        }
        function applyCollapsedState(sectionId, gridEl, chevronEl) {
            const state = getCollapsedState();
            if (state[sectionId]) {
                gridEl.classList.add('hidden');
                if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
            } else {
                gridEl.classList.remove('hidden');
                if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
            }
        }

        function switchTab(tab) {
            currentTab = tab;
            document.querySelectorAll('.tab-btn').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.tab === tab);
            });
            document.getElementById('projectsTab').classList.toggle('hidden', tab !== 'projects');
            document.getElementById('systemTab').classList.toggle('hidden', tab !== 'system');
            document.getElementById('cronTab').classList.toggle('hidden', tab !== 'cron');
            document.getElementById('dockerTab').classList.toggle('hidden', tab !== 'docker');
            
            if (tab === 'docker') loadDockerData();
            
            // 切换Tab时关闭实时监控
            if (tab !== 'system' && realtimeTimer) {
                clearInterval(realtimeTimer);
                realtimeTimer = null;
                document.getElementById('realtimeCheck').checked = false;
                document.getElementById('showProcessListCheck').checked = false;
                document.getElementById('realtimeToggle').classList.remove('active');
                document.getElementById('realtimeIndicator').classList.add('hidden');
                document.getElementById('processListSection').classList.add('hidden');
            }
            
            // 离开系统Tab时关闭非实时刷新定时器
            if (tab !== 'system' && systemRefreshTimer) {
                clearInterval(systemRefreshTimer);
                systemRefreshTimer = null;
            }
            
            if (tab === 'cron') {
                loadCronTasks();
            }
            if (tab === 'system') {
                loadThresholds();
                loadDesktopNotifySetting();
                renderEmailRules();
                loadSystemStats();
                // 启动系统资源定时刷新（按页面刷新间隔）
                startSystemRefresh();
            }
        }

        // 加载阈值设置
        async function loadThresholds() {
            try {
                const response = await fetch('/api/alerts');
                const config = await response.json();
                
                if (config.thresholds) {
                    thresholds = config.thresholds;
                    document.getElementById('thresholdCpu').value = thresholds.cpu || 80;
                    document.getElementById('thresholdCpuTemp').value = thresholds.cpu_temp || 80;
                    document.getElementById('thresholdMemory').value = thresholds.memory || 85;
                    document.getElementById('thresholdDisk').value = thresholds.disk || 90;
                    document.getElementById('thresholdInterval').value = thresholds.interval || 60;
                }
            } catch (e) {
                console.error('加载阈值配置失败:', e);
            }
        }
        
        // 保存阈值设置
        async function saveThresholds() {
            thresholds.cpu = parseInt(document.getElementById('thresholdCpu').value) || 80;
            thresholds.cpu_temp = parseInt(document.getElementById('thresholdCpuTemp').value) || 80;
            thresholds.memory = parseInt(document.getElementById('thresholdMemory').value) || 85;
            thresholds.disk = parseInt(document.getElementById('thresholdDisk').value) || 90;
            thresholds.interval = parseInt(document.getElementById('thresholdInterval').value) || 60;
            
            try {
                // 先获取现有配置
                let config = {};
                try {
                    const response = await fetch('/api/alerts');
                    config = await response.json();
                } catch (e) {}
                
                // 更新阈值
                config.thresholds = thresholds;
                
                // 保存到服务器
                await fetch('/api/alerts', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(config)
                });
            } catch (e) {
                console.error('保存阈值配置失败:', e);
            }
        }
        
        // 桌面通知开关（已在外部声明 desktopNotifyEnabled）
        
        function toggleDesktopNotify() {
            const checked = document.getElementById('enableDesktopNotify').checked;
            const status = document.getElementById('notifyStatus');
            
            if (checked) {
                // 检查浏览器支持
                if (!('Notification' in window)) {
                    status.textContent = '不支持';
                    status.className = 'text-xs text-red-500 ml-1';
                    document.getElementById('enableDesktopNotify').checked = false;
                    return;
                }
                
                // 检查权限状态
                if (Notification.permission === 'granted') {
                    desktopNotifyEnabled = true;
                    status.textContent = '已启用';
                    status.className = 'text-xs text-green-400 ml-1';
                    localStorage.setItem('desktopNotify', 'true');
                } else if (Notification.permission === 'denied') {
                    status.textContent = '已拒绝';
                    status.className = 'text-xs text-red-500 ml-1';
                    document.getElementById('enableDesktopNotify').checked = false;
                } else {
                    // 请求权限
                    Notification.requestPermission().then(permission => {
                        if (permission === 'granted') {
                            desktopNotifyEnabled = true;
                            status.textContent = '已启用';
                            status.className = 'text-xs text-green-400 ml-1';
                            localStorage.setItem('desktopNotify', 'true');
                        } else {
                            status.textContent = '已拒绝';
                            status.className = 'text-xs text-red-500 ml-1';
                            document.getElementById('enableDesktopNotify').checked = false;
                        }
                    });
                }
            } else {
                desktopNotifyEnabled = false;
                status.textContent = '';
                localStorage.setItem('desktopNotify', 'false');
            }
        }
        
        // 加载桌面通知设置
        function loadDesktopNotifySetting() {
            const saved = localStorage.getItem('desktopNotify');
            if (saved === 'true' && Notification.permission === 'granted') {
                desktopNotifyEnabled = true;
                document.getElementById('enableDesktopNotify').checked = true;
                document.getElementById('notifyStatus').textContent = '已启用';
                document.getElementById('notifyStatus').className = 'text-xs text-green-400 ml-1';
            }
        }
        
        // 发送桌面通知
        function sendDesktopNotification(warnings) {
            if (!desktopNotifyEnabled || Notification.permission !== 'granted') return;
            
            const notification = new Notification('⚠️ 系统资源告警', {
                body: warnings.join('\\n'),
                icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">📊</text></svg>',
                tag: 'system-alert',
                requireInteraction: false
            });
            
            // 点击后聚焦窗口
            notification.onclick = () => {
                window.focus();
                notification.close();
            };
            
            // 5秒后自动关闭
            setTimeout(() => notification.close(), 5000);
        }
        
        // 检查阈值并弹窗警告
        function checkThresholds(data) {
            const now = Date.now();
            const intervalMs = thresholds.interval * 1000;
            
            // 检查是否超过间隔时间
            if (now - lastAlertTime < intervalMs) return;
            
            const warnings = [];
            
            if (data.cpu.percent >= thresholds.cpu) {
                warnings.push(`CPU使用率 ${data.cpu.percent.toFixed(1)}% 超过阈值 ${thresholds.cpu}%`);
            }
            if (data.memory.percent >= thresholds.memory) {
                warnings.push(`内存使用率 ${data.memory.percent.toFixed(1)}% 超过阈值 ${thresholds.memory}%`);
            }
            if (data.disk.percent >= thresholds.disk) {
                warnings.push(`磁盘使用率 ${data.disk.percent.toFixed(1)}% 超过阈值 ${thresholds.disk}%`);
            }
            if (data.cpu.temp_package_0 !== null && data.cpu.temp_package_0 >= thresholds.cpu_temp) {
                warnings.push(`CPU0 温度 ${data.cpu.temp_package_0.toFixed(1)}°C 超过阈值 ${thresholds.cpu_temp}°C`);
            }
            if (data.cpu.temp_package_1 !== null && data.cpu.temp_package_1 >= thresholds.cpu_temp) {
                warnings.push(`CPU1 温度 ${data.cpu.temp_package_1.toFixed(1)}°C 超过阈值 ${thresholds.cpu_temp}°C`);
            }
            
            if (warnings.length > 0) {
                lastAlertTime = now;
                showAlertPopup(warnings);
            }
            
            // 检查邮件通知规则
            checkEmailRules(data);
        }
        
        // 显示警告弹窗（同时发送桌面通知）
        function showAlertPopup(warnings) {
            // 发送桌面通知
            sendDesktopNotification(warnings);
            
            // 移除已有弹窗
            const existing = document.querySelector('.alert-popup');
            if (existing) existing.remove();
            
            const popup = document.createElement('div');
            popup.className = 'alert-popup';
            popup.innerHTML = `
                <span class="alert-close" onclick="this.parentElement.remove()"><i class="ri-close-line"></i></span>
                <div class="alert-title"><i class="ri-alarm-warning-line"></i> 资源告警</div>
                <div class="alert-content">${warnings.map(w => `<p>⚠️ ${w}</p>`).join('')}</div>
            `;
            document.body.appendChild(popup);
            
            // 自动关闭（10秒后）
            setTimeout(() => {
                popup.style.animation = 'slideOut 0.3s ease-out forwards';
                setTimeout(() => popup.remove(), 300);
            }, 10000);
        }
        
        function updateRealtimeInterval() {
            realtimeInterval = parseInt(document.getElementById('realtimeIntervalSelect').value) || 2;
            // 如果正在监控，重新启动定时器
            if (realtimeTimer) {
                clearInterval(realtimeTimer);
                realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
            }
        }
        
        async function loadSystemStats() {
            try {
                const res = await fetch('/api/system/stats');
                const data = await res.json();
                
                if (!data.available) {
                    document.getElementById('sysUptime').textContent = 'psutil未安装';
                    document.getElementById('sysBootTime').textContent = '-';
                    return;
                }
                
                // 系统概览
                document.getElementById('sysUptime').textContent = data.system.uptime;
                document.getElementById('sysBootTime').textContent = data.system.boot_time;
                document.getElementById('sysProcessCount').textContent = data.system.process_count;
                document.getElementById('sysCpuCount').textContent = data.cpu.logical + '核';
                
                // CPU
                document.getElementById('cpuPercent').textContent = data.cpu.percent.toFixed(1) + '%';
                document.getElementById('cpuBar').style.width = data.cpu.percent + '%';
                document.getElementById('cpuPhysical').textContent = data.cpu.count;
                document.getElementById('cpuLogical').textContent = data.cpu.logical;
                // CPU 温度
                if (data.cpu.temp_package_0 !== null) {
                    document.getElementById('cpuTemp0').textContent = data.cpu.temp_package_0.toFixed(1);
                }
                if (data.cpu.temp_package_1 !== null) {
                    document.getElementById('cpuTemp1').textContent = data.cpu.temp_package_1.toFixed(1);
                }
                
                // 内存
                document.getElementById('memPercent').textContent = data.memory.percent.toFixed(1) + '%';
                document.getElementById('memBar').style.width = data.memory.percent + '%';
                document.getElementById('memUsed').textContent = data.memory.used_gb;
                document.getElementById('memTotal').textContent = data.memory.total_gb;
                document.getElementById('memAvailable').textContent = data.memory.available_gb;
                
                // 磁盘
                document.getElementById('diskPercent').textContent = data.disk.percent.toFixed(1) + '%';
                document.getElementById('diskBar').style.width = data.disk.percent + '%';
                document.getElementById('diskUsed').textContent = data.disk.used_gb;
                document.getElementById('diskTotal').textContent = data.disk.total_gb;
                document.getElementById('diskFree').textContent = data.disk.free_gb;
                
                // 网络 - 累计流量
                document.getElementById('netSent').textContent = data.network.sent_mb;
                document.getElementById('netRecv').textContent = data.network.recv_mb;
                // 实时网速
                document.getElementById('netSentSpeed').textContent = data.network.sent_speed || '-';
                document.getElementById('netRecvSpeed').textContent = data.network.recv_speed || '-';
                
                // 检查阈值
                checkThresholds(data);
                
                // 如果实时监控和进程列表都开启，加载进程列表
                if (document.getElementById('realtimeCheck').checked && document.getElementById('showProcessListCheck').checked) {
                    loadTopProcesses();
                }
                
            } catch (e) {
                console.error('系统资源加载失败:', e);
            }
        }
        
        // 加载CPU占用最高的进程
        async function loadTopProcesses() {
            try {
                const limit = document.getElementById('processLimitSelect').value;
                const res = await fetch(`/api/system/processes?limit=${limit}`);
                const data = await res.json();
                
                if (!data.available) {
                    document.getElementById('processList').innerHTML = '<div class="text-red-400 text-center py-4">无法获取进程信息</div>';
                    return;
                }
                
                const list = document.getElementById('processList');
                if (data.processes.length === 0) {
                    list.innerHTML = '<div class="text-gray-500 text-center py-4">暂无进程数据</div>';
                    return;
                }
                
                list.innerHTML = data.processes.map(proc => `
                    <div class="card rounded-lg p-3 hover:border-gray-500 transition-colors">
                        <div class="flex items-center justify-between">
                            <div class="flex items-center gap-3 min-w-0">
                                <span class="text-purple-400 font-mono text-sm">${proc.pid}</span>
                                <div class="min-w-0 flex-1">
                                    <div class="text-gray-200 truncate">${proc.name}</div>
                                    <div class="text-gray-500 text-xs truncate">${proc.cmdline || proc.exe}</div>
                                </div>
                            </div>
                            <div class="flex items-center gap-4 text-xs shrink-0">
                                <div class="text-right">
                                    <span class="text-blue-400 font-bold">${proc.cpu.toFixed(1)}%</span>
                                    <span class="text-gray-500 ml-1">CPU</span>
                                </div>
                                <div class="text-right">
                                    <span class="text-green-400 font-bold">${proc.memory.toFixed(1)}%</span>
                                    <span class="text-gray-500 ml-1">内存</span>
                                </div>
                                <div class="text-gray-400">${proc.create_time}</div>
                            </div>
                        </div>
                    </div>
                `).join('');
            } catch (e) {
                console.error('进程列表加载失败:', e);
            }
        }
        
        function toggleRealtime() {
            const checked = document.getElementById('realtimeCheck').checked;
            const toggle = document.getElementById('realtimeToggle');
            const indicator = document.getElementById('realtimeIndicator');
            
            // 先停止系统定时刷新
            if (systemRefreshTimer) {
                clearInterval(systemRefreshTimer);
                systemRefreshTimer = null;
            }
            
            if (checked) {
                toggle.classList.add('active');
                indicator.classList.remove('hidden');
                loadSystemStats();
                realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
                if (document.getElementById('showProcessListCheck').checked) {
                    document.getElementById('processListSection').classList.remove('hidden');
                    loadTopProcesses();
                }
            } else {
                toggle.classList.remove('active');
                indicator.classList.add('hidden');
                document.getElementById('processListSection').classList.add('hidden');
                if (realtimeTimer) {
                    clearInterval(realtimeTimer);
                    realtimeTimer = null;
                }
                // 关闭实时后，按页面刷新间隔继续刷新系统资源
                startSystemRefresh();
            }
        }
        
        function startSystemRefresh() {
            // 先停掉旧定时器
            if (systemRefreshTimer) {
                clearInterval(systemRefreshTimer);
                systemRefreshTimer = null;
            }
            // 如果在实时监控模式则不启动（由realtimeTimer负责）
            if (document.getElementById('realtimeCheck').checked) return;
            // 按页面刷新间隔定时刷新
            const intervalSec = parseInt(document.getElementById('refreshInterval').value) || 30;
            systemRefreshTimer = setInterval(loadSystemStats, intervalSec * 1000);
        }
        
        function toggleProcessList() {
            const checked = document.getElementById('showProcessListCheck').checked;
            const realtimeChecked = document.getElementById('realtimeCheck').checked;
            const processSection = document.getElementById('processListSection');
            
            if (checked && realtimeChecked) {
                processSection.classList.remove('hidden');
                loadTopProcesses();
            } else {
                processSection.classList.add('hidden');
            }
        }

        // IP 保存
        async function saveExternalIp() {
            externalIp = document.getElementById('externalIp').value.trim();
            
            try {
                await fetch('/api/web-settings', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ external_ip: externalIp, refresh_interval: parseInt(document.getElementById('refreshInterval').value) || 30 })
                });
            } catch (e) {
                console.error('保存失败:', e);
            }
            
            renderProjects();
        }

        // 项目加载和渲染（保持原有逻辑）
        async function loadProjects() {
            try {
                const res = await fetch('/api/projects');
                const data = await res.json();
                projects = data.projects;
                renderProjects();
                updateStats();
                updateConnectionStatus(true);
                
                // 同时检查系统资源阈值（主页也有效）
                loadThresholds();
                fetch('/api/system/stats').then(r => r.json()).then(sysData => {
                    if (sysData.available) checkThresholds(sysData);
                }).catch(() => {});
                // 同时检查服务下线邮件规则
                checkEmailRules(null);
                
            } catch (e) {
                console.error('加载失败:', e);
                updateConnectionStatus(false);
            }
        }

        function updateStats() {
            const webProjects = projects.filter(p => p.type === 'web');
            const runningProjects = webProjects.filter(p => p.status?.status === 'running');
            const cronProjects = projects.filter(p => p.type === 'cron');
            const cliProjects = projects.filter(p => p.type === 'cli' || p.type === 'extension');
            
            document.getElementById('webCount').textContent = webProjects.length;
            document.getElementById('runningCount').textContent = `${runningProjects.length}/${webProjects.length}`;
            document.getElementById('cronCount').textContent = cronProjects.length;
            document.getElementById('cliCount').textContent = cliProjects.length;
            document.getElementById('updateTime').textContent = new Date().toLocaleTimeString();
        }

        function filterType(type) {
            currentFilter = type;
            document.querySelectorAll('.filter-bar-btn').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.type === type);
            });
            renderProjects();
        }

        function renderProjects() {
            const list = document.getElementById('projectsList');
            
            // 合合自定义服务到项目列表
            const customServices = getCustomServices();
            const allProjects = [...projects, ...customServices];
            
            const filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
            
            if (filtered.length === 0) {
                list.innerHTML = '<div class="text-center py-12 text-gray-400"><i class="ri-folder-open-line text-4xl"></i><p class="mt-2">暂无项目</p></div>';
                return;
            }

            const activeStatus = getActiveStatus();
            const grouped = {};
            filtered.forEach(p => {
                const groupType = p.isCustom ? 'custom' : p.type;
                if (!grouped[groupType]) grouped[groupType] = [];
                grouped[groupType].push(p);
            });

            let html = '';
            const typeNames = {
                'web': { name: 'Web服务', icon: 'ri-server-line', color: 'blue' },
                'custom': { name: '自定义服务', icon: 'ri-global-line', color: 'cyan' },
                'cron': { name: 'Cron任务', icon: 'ri-time-line', color: 'yellow' },
                'cli': { name: 'CLI工具', icon: 'ri-terminal-box-line', color: 'purple' },
                'extension': { name: '浏览器插件', icon: 'ri-chrome-line', color: 'pink' }
            };

            for (const [type, projs] of Object.entries(grouped)) {
                const typeInfo = typeNames[type] || { name: type, icon: 'ri-folder-line', color: 'gray' };
                
                if (type === 'web') {
                    const activeProjs = projs.filter(p => activeStatus[p.id] !== false);
                    const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
                    
                    if (activeProjs.length > 0) {
                        const secId = 'section-web-active';
                        html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
                        activeProjs.forEach(p => { html += renderProjectCard(p, true); });
                        html += '</div></div>';
                    }
                    
                    if (archivedProjs.length > 0) {
                        const secId = 'section-web-archived';
                        html += `<div class="mb-6 opacity-60"><h2 class="text-lg font-semibold text-gray-400 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="ri-archive-line text-gray-400"></i>归档服务<span class="text-sm text-gray-500">(${archivedProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
                        archivedProjs.forEach(p => { html += renderProjectCard(p, false); });
                        html += '</div></div>';
                    }
                } else {
                    const secId = 'section-' + type;
                    html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${projs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
                    projs.forEach(p => { html += renderProjectCard(p, true); });
                    html += '</div></div>';
                }
            }
            list.innerHTML = html;
            // 应用已保存的折叠状态
            setTimeout(() => {
                const state = getCollapsedState();
                for (const [secId, collapsed] of Object.entries(state)) {
                    const grid = document.getElementById(secId);
                    const chevron = grid?.parentElement?.querySelector('.chevron');
                    if (grid && collapsed) {
                        grid.classList.add('hidden');
                        if (chevron) chevron.style.transform = 'rotate(-90deg)';
                    }
                }
            }, 10);
        }

        function getActiveStatus() {
            const stored = localStorage.getItem('serviceActiveStatus');
            return stored ? JSON.parse(stored) : {};
        }

        function toggleServiceActive(id) {
            const status = getActiveStatus();
            status[id] = status[id] === false ? true : false;
            localStorage.setItem('serviceActiveStatus', JSON.stringify(status));
            renderProjects();
        }

        function renderProjectCard(p, isActive = true) {
            // 自定义服务特殊处理
            if (p.isCustom) {
                return `
                    <div class="card rounded-lg p-3 hover:border-cyan-500 transition-colors border-cyan-500/30">
                        <div class="flex items-center justify-between mb-2">
                            <div class="flex items-center gap-2">
                                <div class="status-dot bg-cyan-400" style="box-shadow: 0 0 8px #22d3ee;" title="外部服务"></div>
                                <h3 class="font-semibold text-sm truncate">${p.name}</h3>
                            </div>
                            <span class="text-xs text-cyan-400">外部</span>
                        </div>
                        <p class="text-gray-400 text-xs mb-2 truncate">${p.description || ''}</p>
                        <div class="flex items-center gap-1 text-xs flex-wrap">
                            <a href="${p.url}" target="_blank" class="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30">访问</a>
                            ${p.admin_url ? `<a href="${p.admin_url}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
                            <button onclick="deleteCustomService('${p.id}')" class="text-red-400 hover:text-red-300 ml-2" title="删除服务"><i class="ri-delete-bin-line"></i></button>
                        </div>
                    </div>
                `;
            }
            
            const statusInfo = getStatusInfo(p.status?.status);
            
            // 处理后台链接：将 localhost 替换为 externalIp
            let adminUrl = p.admin_url || '';
            if (adminUrl && adminUrl.includes('localhost')) {
                adminUrl = adminUrl.replace(/localhost/g, externalIp);
            }
            
            // 获取自定义链接
            const customLinks = getCustomLinks(p.id);
            
            return `
                <div class="card rounded-lg p-3 hover:border-gray-500 transition-colors">
                    <div class="flex items-center justify-between mb-2">
                        <div class="flex items-center gap-2">
                            <div class="status-dot ${statusInfo.class}" title="${statusInfo.text}"></div>
                            <h3 class="font-semibold text-sm truncate">${p.name}</h3>
                        </div>
                        <span class="text-xs ${statusInfo.textColor}">${statusInfo.text}</span>
                    </div>
                    ${p.ports && p.ports.length > 0 ? `
                    <div class="flex items-center gap-1 text-xs mb-2 flex-wrap">
                        ${p.ports.map(port => {
                            const portStatus = p.status?.ports?.[port];
                            const isRunning = portStatus?.running;
                            return `<a href="http://${externalIp}:${port}" target="_blank" class="px-2 py-0.5 rounded ${isRunning ? 'bg-green-500/20 text-green-400 hover:bg-green-500/30' : 'bg-red-500/20 text-red-400'}">${port}</a>`;
                        }).join('')}
                        ${adminUrl ? `<a href="${adminUrl}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
                        ${customLinks.map(link => `<a href="${link.url}" target="_blank" class="text-cyan-400 hover:text-cyan-300 ml-1">${link.name}</a>`).join('')}
                        <button onclick="showAddLinkModal('${p.id}', '${p.name}')" class="text-green-400 hover:text-green-300 ml-1" title="添加自定义链接"><i class="ri-add-line"></i></button>
                    </div>
                    ` : ''}
                    ${p.type === 'web' ? `
                    <div class="flex items-center justify-between mt-2">
                        <div class="flex items-center gap-1">
                            ${(p.status?.status === 'running' || p.status?.status === 'partial') ? `
                                <button onclick="stopProject('${p.id}')" class="btn bg-red-600 hover:bg-red-700 px-2 py-0.5 rounded text-xs">停止</button>
                                <button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-2 py-0.5 rounded text-xs">重启</button>
                            ` : `<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-2 py-0.5 rounded text-xs">启动</button>`}
                            <button onclick="viewLog('${p.id}')" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-0.5 rounded text-xs">日志</button>
                        </div>
                        <button onclick="toggleServiceActive('${p.id}')" class="text-xs ${isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-400 hover:text-gray-300'}" title="${isActive ? '点击归档' : '点击激活'}">
                            <i class="ri-${isActive ? 'checkbox-circle' : 'archive'}-line"></i>
                        </button>
                    </div>
                    ` : ''}
                </div>
            `;
        }
        
        // ==================== 自定义链接管理 ====================
        
        function getCustomLinks(projectId) {
            const stored = localStorage.getItem('customLinks_' + projectId);
            return stored ? JSON.parse(stored) : [];
        }
        
        function saveCustomLinks(projectId, links) {
            localStorage.setItem('customLinks_' + projectId, JSON.stringify(links));
        }
        
        function showAddLinkModal(projectId, projectName) {
            document.getElementById('linkModalTitle').textContent = `${projectName} - 添加链接`;
            document.getElementById('linkProjectId').value = projectId;
            document.getElementById('linkName').value = '';
            document.getElementById('linkPort').value = '';
            document.getElementById('linkPath').value = '';
            document.getElementById('linkFullUrl').value = '';
            document.getElementById('linkModal').classList.remove('hidden');
            
            // 显示已有的自定义链接（供删除）
            const existingLinks = getCustomLinks(projectId);
            const existingList = document.getElementById('existingLinks');
            if (existingLinks.length > 0) {
                existingList.innerHTML = '<div class="text-gray-400 text-sm mb-2">已有链接：</div>' +
                    existingLinks.map(link => `
                        <div class="flex items-center justify-between bg-gray-700 px-2 py-1 rounded mb-1">
                            <span class="text-cyan-400 text-sm">${link.name}: ${link.url}</span>
                            <button onclick="deleteCustomLink('${projectId}', '${link.id}')" class="text-red-400 hover:text-red-300"><i class="ri-delete-bin-line"></i></button>
                        </div>
                    `).join('');
            } else {
                existingList.innerHTML = '';
            }
        }
        
        function closeLinkModal() {
            document.getElementById('linkModal').classList.add('hidden');
        }
        
        function saveCustomLink(event) {
            event.preventDefault();
            
            const projectId = document.getElementById('linkProjectId').value;
            const name = document.getElementById('linkName').value.trim();
            const port = document.getElementById('linkPort').value.trim();
            const path = document.getElementById('linkPath').value.trim();
            const fullUrl = document.getElementById('linkFullUrl').value.trim();
            
            if (!name) {
                alert('请输入链接名称');
                return;
            }
            
            let url;
            if (fullUrl) {
                url = fullUrl;
            } else if (port) {
                url = `http://${externalIp}:${port}${path || ''}`;
            } else {
                alert('请输入端口或完整URL');
                return;
            }
            
            const links = getCustomLinks(projectId);
            const newLink = {
                id: Date.now().toString(),
                name: name,
                url: url
            };
            links.push(newLink);
            saveCustomLinks(projectId, links);
            
            closeLinkModal();
            renderProjects();
        }
        
        function deleteCustomLink(projectId, linkId) {
            if (!confirm('确定删除此链接？')) return;
            
            const links = getCustomLinks(projectId);
            const filtered = links.filter(l => l.id !== linkId);
            saveCustomLinks(projectId, filtered);
            
            // 更新模态框中的显示
            const existingLinks = getCustomLinks(projectId);
            const existingList = document.getElementById('existingLinks');
            if (existingLinks.length > 0) {
                existingList.innerHTML = '<div class="text-gray-400 text-sm mb-2">已有链接：</div>' +
                    existingLinks.map(link => `
                        <div class="flex items-center justify-between bg-gray-700 px-2 py-1 rounded mb-1">
                            <span class="text-cyan-400 text-sm">${link.name}: ${link.url}</span>
                            <button onclick="deleteCustomLink('${projectId}', '${link.id}')" class="text-red-400 hover:text-red-300"><i class="ri-delete-bin-line"></i></button>
                        </div>
                    `).join('');
            } else {
                existingList.innerHTML = '<div class="text-gray-500 text-sm">暂无自定义链接</div>';
            }
            
            renderProjects();
        }
        
        // 自动生成URL预览
        function updateLinkPreview() {
            const port = document.getElementById('linkPort').value.trim();
            const path = document.getElementById('linkPath').value.trim();
            const preview = document.getElementById('linkPreview');
            
            if (port) {
                preview.textContent = `预览: http://${externalIp}:${port}${path || ''}`;
            } else {
                preview.textContent = '';
            }
        }

        // ==================== 自定义服务管理 ====================
        
        function getCustomServices() {
            const stored = localStorage.getItem('customServices');
            return stored ? JSON.parse(stored) : [];
        }
        
        function saveCustomServices(services) {
            localStorage.setItem('customServices', JSON.stringify(services));
        }
        
        // ==================== 邮件通知规则管理 ====================
        
        function getEmailRules() {
            const stored = localStorage.getItem('emailNotifyRules');
            return stored ? JSON.parse(stored) : [];
        }
        
        function saveEmailRules(rules) {
            localStorage.setItem('emailNotifyRules', JSON.stringify(rules));
        }
        
        function renderEmailRules() {
            const rules = getEmailRules();
            const list = document.getElementById('emailRulesList');
            
            if (rules.length === 0) {
                list.innerHTML = '<div class="text-gray-500 text-center py-4">暂无邮件通知规则</div>';
                return;
            }
            
            const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘', 'service_down': '服务下线' };
            const resourceUnits = { 'cpu': '%', 'cpu_temp': '°C', 'memory': '%', 'disk': '%' };
            
            list.innerHTML = rules.map(rule => {
                const silentInfo = rule.silentEnabled ? `静默:${rule.silentStart}-${rule.silentEnd}` : '';
                const triggerType = rule.triggerType || 'single';
                const triggerParam = rule.triggerParam || 3;
                let triggerDisplay = '一次性';
                if (triggerType === 'continuous') triggerDisplay = `连续${triggerParam}次`;
                else if (triggerType === 'duration') triggerDisplay = `持续${triggerParam}秒`;
                else if (triggerType === 'average') triggerDisplay = `平均${triggerParam}次`;
                
                // 显示资源列表（支持多资源和旧版单资源兼容）
                const resources = rule.resources || (rule.resource ? [rule.resource] : []);
                let resourceDisplay;
                if (resources.includes('service_down') && rule.serviceName) {
                    resourceDisplay = `🔴 ${rule.serviceName} 下线 ≥ ${rule.threshold}分钟`;
                } else {
                    resourceDisplay = resources.map(r => resourceNames[r] || r).join('+') + ' ≥ ' + rule.threshold;
                }
                
                return `
                <div class="flex items-center justify-between bg-gray-700/50 px-3 py-2 rounded-lg">
                    <div class="flex items-center gap-3 flex-wrap">
                        <span class="px-2 py-0.5 rounded text-xs ${rule.enabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}">${rule.enabled ? '启用' : '禁用'}</span>
                        <span class="text-gray-200">${rule.name}</span>
                        <span class="text-gray-400 text-xs">${resourceDisplay}</span>
                        <span class="text-blue-400/70 text-xs"><i class="ri-flashlight-line"></i> ${triggerDisplay}</span>
                        <span class="text-gray-400 text-xs">→ ${rule.email}</span>
                        ${silentInfo ? `<span class="text-yellow-400/70 text-xs"><i class="ri-moon-line"></i> ${silentInfo}</span>` : ''}
                    </div>
                    <div class="flex items-center gap-1">
                        <button onclick="editEmailRule('${rule.id}')" class="text-blue-400 hover:text-blue-300 px-1" title="编辑"><i class="ri-edit-line"></i></button>
                        <button onclick="deleteEmailRule('${rule.id}')" class="text-red-400 hover:text-red-300 px-1" title="删除"><i class="ri-delete-bin-line"></i></button>
                    </div>
                </div>
            `;
            }).join('');
        }
        
        function showAddEmailRuleModal() {
            document.getElementById('emailRuleModalTitle').textContent = '添加邮件通知规则';
            document.getElementById('emailRuleId').value = '';
            document.getElementById('emailRuleName').value = '';
            // 重置资源复选框
            document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
            document.getElementById('serviceDownSelector').style.display = 'none';
            document.getElementById('emailRuleServiceId').value = '';
            document.getElementById('emailRuleThreshold').value = 80;
            document.getElementById('emailRuleThresholdUnit').textContent = '% / °C';
            document.getElementById('emailRuleThresholdHint').textContent = '对不同资源类型使用统一阈值';
            document.getElementById('emailRuleTriggerType').value = 'single';
            document.getElementById('emailRuleTriggerType').disabled = false;
            document.getElementById('emailRuleTriggerParam').value = 3;
            document.getElementById('triggerParamsDiv').style.display = 'none';
            document.getElementById('emailRuleAddress').value = '';
            document.getElementById('emailRuleInterval').value = 300;
            document.getElementById('emailRuleSilentStart').value = '23:00';
            document.getElementById('emailRuleSilentEnd').value = '08:00';
            document.getElementById('emailRuleSilentEnabled').checked = false;
            document.getElementById('emailRuleEnabled').checked = true;
            document.getElementById('emailRuleModal').classList.remove('hidden');
        }
        
        function closeEmailRuleModal() {
            document.getElementById('emailRuleModal').classList.add('hidden');
        }
        
        function getSelectedResources() {
            const checkboxes = document.querySelectorAll('.email-rule-resource:checked');
            return Array.from(checkboxes).map(cb => cb.value);
        }
        
        function onResourceChange() {
            const selected = getSelectedResources();
            const unitEl = document.getElementById('emailRuleThresholdUnit');
            const thresholdEl = document.getElementById('emailRuleThreshold');
            const hintEl = document.getElementById('emailRuleThresholdHint');
            const serviceSelector = document.getElementById('serviceDownSelector');
            const triggerTypeEl = document.getElementById('emailRuleTriggerType');
            const triggerParamsDiv = document.getElementById('triggerParamsDiv');
            
            const hasServiceDown = selected.includes('service_down');
            
            // 显示/隐藏服务选择器
            if (hasServiceDown) {
                serviceSelector.style.display = 'block';
                populateServiceSelector();
                // 服务下线使用分钟作为阈值单位
                unitEl.textContent = '分钟';
                thresholdEl.value = 5;
                thresholdEl.max = 1440;
                thresholdEl.min = 1;
                hintEl.textContent = '服务连续离线超过此分钟数即触发通知';
                // 服务下线固定为duration类型
                if (triggerTypeEl) {
                    triggerTypeEl.value = 'duration';
                    triggerTypeEl.disabled = true;
                }
                if (triggerParamsDiv) triggerParamsDiv.style.display = 'none';
            } else {
                serviceSelector.style.display = 'none';
                if (triggerTypeEl) triggerTypeEl.disabled = false;
                updateTriggerParams();
                
                if (selected.length === 0) {
                    unitEl.textContent = '% / °C';
                    hintEl.textContent = '对不同资源类型使用统一阈值';
                } else if (selected.length === 1 && selected[0] === 'cpu_temp') {
                    unitEl.textContent = '°C';
                    if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80;
                    thresholdEl.max = 120;
                    thresholdEl.min = 30;
                    hintEl.textContent = 'CPU温度超过此值即触发通知';
                } else {
                    unitEl.textContent = '%';
                    thresholdEl.max = 100;
                    thresholdEl.min = 1;
                    hintEl.textContent = '对不同资源类型使用统一阈值';
                    if (selected.includes('cpu_temp')) {
                        unitEl.textContent = '% / °C';
                    }
                }
            }
        }
        
        function populateServiceSelector() {
            const sel = document.getElementById('emailRuleServiceId');
            const currentVal = sel.value;
            // 收集所有web类型服务
            const webServices = projects.filter(p => p.type === 'web');
            let options = '<option value="">-- 请选择要监控的服务 --</option>';
            webServices.forEach(p => {
                const portsStr = p.ports ? p.ports.join(',') : '';
                const selected = p.id === currentVal ? ' selected' : '';
                options += `<option value="${p.id}"${selected}>${p.name} (端口: ${portsStr})</option>`;
            });
            sel.innerHTML = options;
        }
        
        function onServiceDownChange() {
            // 将来可以在这里做联动
        }
        
        function updateTriggerParams() {
            const triggerType = document.getElementById('emailRuleTriggerType').value;
            const paramsDiv = document.getElementById('triggerParamsDiv');
            const labelEl = document.getElementById('triggerParamsLabel');
            const unitEl = document.getElementById('triggerParamUnit');
            const hintEl = document.getElementById('triggerParamHint');
            const paramInput = document.getElementById('emailRuleTriggerParam');
            
            // 根据触发类型设置不同的参数
            switch (triggerType) {
                case 'single':
                    paramsDiv.style.display = 'none';
                    break;
                case 'continuous':
                    paramsDiv.style.display = 'flex';
                    labelEl.textContent = '连续次数';
                    unitEl.textContent = '次';
                    hintEl.textContent = '连续超过阈值多少次才触发';
                    paramInput.max = 100;
                    paramInput.value = 3;
                    break;
                case 'duration':
                    paramsDiv.style.display = 'flex';
                    labelEl.textContent = '持续时间';
                    unitEl.textContent = '秒';
                    hintEl.textContent = '累计超过阈值多少秒才触发';
                    paramInput.max = 3600;
                    paramInput.value = 60;
                    break;
                case 'average':
                    paramsDiv.style.display = 'flex';
                    labelEl.textContent = '采样次数';
                    unitEl.textContent = '次';
                    hintEl.textContent = '最近N次采样的平均值超过阈值才触发';
                    paramInput.max = 100;
                    paramInput.value = 5;
                    break;
            }
        }
        
        function editEmailRule(ruleId) {
            const rules = getEmailRules();
            const rule = rules.find(r => r.id === ruleId);
            if (!rule) return;
            
            document.getElementById('emailRuleModalTitle').textContent = '编辑邮件通知规则';
            document.getElementById('emailRuleId').value = rule.id;
            document.getElementById('emailRuleName').value = rule.name;
            // 重置所有复选框
            document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
            // 勾选已保存的资源（支持新版resources数组和旧版resource字符串兼容）
            const resources = rule.resources || (rule.resource ? [rule.resource] : []);
            resources.forEach(r => {
                const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
                if (cb) cb.checked = true;
            });
            onResourceChange();
            // 服务下线相关
            if (rule.serviceId) {
                document.getElementById('emailRuleServiceId').value = rule.serviceId;
            }
            document.getElementById('emailRuleThreshold').value = rule.threshold;
            document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
            document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
            updateTriggerParams();
            document.getElementById('emailRuleAddress').value = rule.email;
            document.getElementById('emailRuleInterval').value = rule.interval;
            document.getElementById('emailRuleSilentStart').value = rule.silentStart || '23:00';
            document.getElementById('emailRuleSilentEnd').value = rule.silentEnd || '08:00';
            document.getElementById('emailRuleSilentEnabled').checked = rule.silentEnabled || false;
            document.getElementById('emailRuleEnabled').checked = rule.enabled;
            document.getElementById('emailRuleModal').classList.remove('hidden');
        }
        
        function saveEmailRule(event) {
            event.preventDefault();
            
            const ruleId = document.getElementById('emailRuleId').value;
            const name = document.getElementById('emailRuleName').value.trim();
            const resources = getSelectedResources();
            const threshold = parseInt(document.getElementById('emailRuleThreshold').value);
            const triggerType = document.getElementById('emailRuleTriggerType').value;
            const triggerParam = parseInt(document.getElementById('emailRuleTriggerParam').value) || 3;
            const email = document.getElementById('emailRuleAddress').value.trim();
            const interval = parseInt(document.getElementById('emailRuleInterval').value) || 300;
            const silentStart = document.getElementById('emailRuleSilentStart').value;
            const silentEnd = document.getElementById('emailRuleSilentEnd').value;
            const silentEnabled = document.getElementById('emailRuleSilentEnabled').checked;
            const enabled = document.getElementById('emailRuleEnabled').checked;
            const serviceId = document.getElementById('emailRuleServiceId').value;
            
            if (!name || !email) {
                alert('请填写完整信息');
                return;
            }
            
            if (resources.length === 0) {
                alert('请至少选择一个监控资源');
                return;
            }
            
            // 服务下线必须选择具体服务
            if (resources.includes('service_down') && !serviceId) {
                alert('请选择要监控的服务');
                return;
            }
            
            const rules = getEmailRules();
            
            if (ruleId) {
                // 编辑
                const idx = rules.findIndex(r => r.id === ruleId);
                if (idx >= 0) {
                    rules[idx] = { ...rules[idx], name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled, serviceId };
                }
            } else {
                // 新增
                const svc = projects.find(p => p.id === serviceId);
                rules.push({
                    id: 'rule_' + Date.now(),
                    name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
                    serviceId, serviceName: svc ? svc.name : '',
                    offlineSince: null,  // 服务下线开始时间
                    lastSent: 0,
                    exceedCount: 0,
                    exceedDuration: 0,
                    recentValues: []
                });
            }
            
            saveEmailRules(rules);
            closeEmailRuleModal();
            renderEmailRules();
        }
        
        function deleteEmailRule(ruleId) {
            if (!confirm('确定删除此邮件通知规则？')) return;
            const rules = getEmailRules().filter(r => r.id !== ruleId);
            saveEmailRules(rules);
            renderEmailRules();
        }
        
        // 检查邮件规则并发送通知
        async function checkEmailRules(data) {
            const rules = getEmailRules().filter(r => r.enabled);
            const now = Date.now();
            const nowTime = new Date();
            const currentHour = nowTime.getHours();
            const currentMin = nowTime.getMinutes();
            const currentTimeStr = `${currentHour.toString().padStart(2, '0')}:${currentMin.toString().padStart(2, '0')}`;
            
            for (const rule of rules) {
                // 检查间隔
                if (now - rule.lastSent < rule.interval * 1000) continue;
                
                // 检查静默时间
                if (rule.silentEnabled && rule.silentStart && rule.silentEnd) {
                    const start = rule.silentStart;
                    const end = rule.silentEnd;
                    
                    // 处理跨越午夜的情况（如23:00到08:00）
                    let inSilentPeriod = false;
                    if (start > end) {
                        // 跨越午夜：如23:00-08:00，则currentTime >= start 或 currentTime < end 都在静默区间
                        inSilentPeriod = (currentTimeStr >= start || currentTimeStr < end);
                    } else {
                        // 同一天内：如01:00-06:00
                        inSilentPeriod = (currentTimeStr >= start && currentTimeStr < end);
                    }
                    
                    if (inSilentPeriod) {
                        console.log(`规则 ${rule.name} 当前在静默时间段 ${start}-${end}，跳过发送`);
                        continue;
                    }
                }
                
                // 检查阈值和触发机制（支持多资源）
                const resources = rule.resources || (rule.resource ? [rule.resource] : []);
                const resourceNames = { 'cpu': 'CPU使用率', 'cpu_temp': 'CPU温度', 'memory': '内存使用率', 'disk': '磁盘使用率' };
                
                // 收集所有超标的资源
                const exceededResources = [];
                
                for (const resource of resources) {
                    let value;
                    
                    // 服务下线监控 - 特殊处理（不需要data参数）
                    if (resource === 'service_down') {
                        const svcId = rule.serviceId;
                        if (!svcId) continue;
                        const svc = projects.find(p => p.id === svcId);
                        if (!svc) continue;
                        const isOnline = svc.status?.status === 'running';
                        
                        if (isOnline) {
                            rule.offlineSince = null;
                        } else {
                            if (!rule.offlineSince) {
                                rule.offlineSince = now;
                            }
                            const offlineMinutes = (now - rule.offlineSince) / 60000;
                            if (offlineMinutes >= rule.threshold) {
                                const svcName = svc.name || rule.serviceName || svcId;
                                exceededResources.push(`🔴 ${svcName} 已离线 ${Math.round(offlineMinutes)} 分钟 (阈值: ${rule.threshold} 分钟)`);
                            }
                        }
                        continue;
                    }
                    
                    // 系统资源监控需要 data 参数
                    if (!data) continue;
                    
                    if (resource === 'cpu_temp') {
                        // CPU温度取两个Package中较高的那个
                        const t0 = data.cpu?.temp_package_0;
                        const t1 = data.cpu?.temp_package_1;
                        if (t0 === null && t1 === null) continue;
                        if (t0 === null) value = t1;
                        else if (t1 === null) value = t0;
                        else value = Math.max(t0, t1);
                    } else {
                        value = data[resource]?.percent;
                    }
                    
                    if (value === undefined || value === null) continue;
                    
                    const isExceedThreshold = value >= rule.threshold;
                    
                    // 初始化触发相关数据（key后缀避免不同资源冲突）
                    const ck = '_' + resource;
                    if (!rule['exceedCount' + ck]) rule['exceedCount' + ck] = 0;
                    if (!rule['exceedDuration' + ck]) rule['exceedDuration' + ck] = 0;
                    if (!rule['recentValues' + ck]) rule['recentValues' + ck] = [];
                    
                    const triggerType = rule.triggerType || 'single';
                    const triggerParam = rule.triggerParam || 3;
                    let resourceTriggered = false;
                    
                    switch (triggerType) {
                        case 'single':
                            if (isExceedThreshold) resourceTriggered = true;
                            break;
                        case 'continuous':
                            if (isExceedThreshold) {
                                rule['exceedCount' + ck]++;
                                if (rule['exceedCount' + ck] >= triggerParam) {
                                    resourceTriggered = true;
                                    rule['exceedCount' + ck] = 0;
                                }
                            } else {
                                rule['exceedCount' + ck] = 0;
                            }
                            break;
                        case 'duration':
                            if (isExceedThreshold) {
                                const intervalSec = realtimeInterval || 2;
                                rule['exceedDuration' + ck] += intervalSec;
                                if (rule['exceedDuration' + ck] >= triggerParam) {
                                    resourceTriggered = true;
                                    rule['exceedDuration' + ck] = 0;
                                }
                            } else {
                                rule['exceedDuration' + ck] = 0;
                            }
                            break;
                        case 'average':
                            rule['recentValues' + ck].push(value);
                            if (rule['recentValues' + ck].length > triggerParam) {
                                rule['recentValues' + ck].shift();
                            }
                            if (rule['recentValues' + ck].length >= triggerParam) {
                                const avg = rule['recentValues' + ck].reduce((a, b) => a + b, 0) / rule['recentValues' + ck].length;
                                if (avg >= rule.threshold) resourceTriggered = true;
                            }
                            break;
                    }
                    
                    if (resourceTriggered) {
                        const unit = resource === 'cpu_temp' ? '°C' : '%';
                        exceededResources.push(`${resourceNames[resource]}: ${value.toFixed(1)}${unit} (阈值: ${rule.threshold}${unit})`);
                    }
                }
                
                // 保存更新的规则数据
                saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r));
                
                if (exceededResources.length === 0) continue;
                
                // 发送邮件
                try {
                    const res = await fetch('/api/email/send', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            to: rule.email,
                            subject: `[系统告警] ${rule.name}`,
                            body: `${rule.name} 触发告警\n\n${exceededResources.join('\n')}\n\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
                        })
                    });
                    
                    if (res.ok) {
                        // 更新发送时间
                        rule.lastSent = now;
                        saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r));
                    }
                } catch (e) {
                    console.error('邮件发送失败:', e);
                }
            }
        }
        
        // ==================== 测试邮件发送 ====================
        
        function showTestEmailModal() {
            document.getElementById('testEmailAddress').value = '';
            document.getElementById('testEmailSubject').value = '[测试邮件] 项目服务管理面板';
            document.getElementById('testEmailBody').value = '这是一封测试邮件，用于验证邮件通知功能是否正常工作。\n\n发送自: 项目服务管理面板\n时间: ' + new Date().toLocaleString();
            document.getElementById('testEmailResult').classList.add('hidden');
            document.getElementById('testEmailModal').classList.remove('hidden');
        }
        
        function closeTestEmailModal() {
            document.getElementById('testEmailModal').classList.add('hidden');
        }
        
        async function sendTestEmail(event) {
            event.preventDefault();
            
            const email = document.getElementById('testEmailAddress').value.trim();
            const subject = document.getElementById('testEmailSubject').value.trim();
            const body = document.getElementById('testEmailBody').value.trim();
            const resultEl = document.getElementById('testEmailResult');
            const submitBtn = event.target.querySelector('button[type="submit"]');
            
            if (!email) {
                alert('请输入收件邮箱');
                return;
            }
            
            submitBtn.disabled = true;
            submitBtn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> 发送中...';
            resultEl.classList.add('hidden');
            
            try {
                const res = await fetch('/api/email/send', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ to: email, subject, body })
                });
                
                const data = await res.json();
                
                if (res.ok) {
                    resultEl.className = 'mb-3 p-3 rounded-lg bg-green-500/20 text-green-400 text-sm';
                    resultEl.innerHTML = `<i class="ri-checkbox-circle-line"></i> 测试邮件发送成功！<br>收件人: ${email}`;
                } else {
                    resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
                    resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 发送失败: ${data.error || '未知错误'}`;
                }
                resultEl.classList.remove('hidden');
            } catch (e) {
                resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
                resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 网络错误: ${e.message}`;
                resultEl.classList.remove('hidden');
            } finally {
                submitBtn.disabled = false;
                submitBtn.innerHTML = '<i class="ri-send-plane-line"></i> 发送测试';
            }
        }
        
        function showAddServiceModal() {
            document.getElementById('serviceName').value = '';
            document.getElementById('servicePort').value = '';
            document.getElementById('servicePath').value = '';
            document.getElementById('serviceFullUrl').value = '';
            document.getElementById('serviceAdminUrl').value = '';
            document.getElementById('serviceDesc').value = '';
            document.getElementById('servicePreview').textContent = '';
            document.getElementById('addServiceModal').classList.remove('hidden');
        }
        
        function closeAddServiceModal() {
            document.getElementById('addServiceModal').classList.add('hidden');
        }
        
        function updateServicePreview() {
            const port = document.getElementById('servicePort').value.trim();
            const path = document.getElementById('servicePath').value.trim();
            const preview = document.getElementById('servicePreview');
            
            if (port) {
                preview.textContent = `预览: http://${externalIp}:${port}${path || ''}`;
            } else {
                preview.textContent = '';
            }
        }
        
        function saveCustomService(event) {
            event.preventDefault();
            
            const name = document.getElementById('serviceName').value.trim();
            const port = document.getElementById('servicePort').value.trim();
            const path = document.getElementById('servicePath').value.trim();
            const fullUrl = document.getElementById('serviceFullUrl').value.trim();
            const adminUrl = document.getElementById('serviceAdminUrl').value.trim();
            const desc = document.getElementById('serviceDesc').value.trim();
            
            if (!name) {
                alert('请输入服务名称');
                return;
            }
            
            let mainUrl;
            if (fullUrl) {
                mainUrl = fullUrl;
            } else if (port) {
                mainUrl = `http://${externalIp}:${port}${path || ''}`;
            } else {
                alert('请输入端口或完整URL');
                return;
            }
            
            const services = getCustomServices();
            const newService = {
                id: 'custom_' + Date.now(),
                name: name,
                port: port || '',
                url: mainUrl,
                admin_url: adminUrl || null,
                description: desc || '自定义外部服务',
                type: 'custom',
                isCustom: true
            };
            services.push(newService);
            saveCustomServices(services);
            
            closeAddServiceModal();
            loadProjects();
        }
        
        function deleteCustomService(serviceId) {
            if (!confirm('确定删除此自定义服务？')) return;
            
            const services = getCustomServices();
            const filtered = services.filter(s => s.id !== serviceId);
            saveCustomServices(filtered);
            
            loadProjects();
        }

        function getStatusInfo(status) {
            const map = {
                'running': { text: '运行中', class: 'status-running', textColor: 'text-green-400' },
                'stopped': { text: '已停止', class: 'status-stopped', textColor: 'text-red-400' },
                'partial': { text: '部分运行', class: 'status-partial', textColor: 'text-yellow-400' },
                'ready': { text: '就绪', class: 'status-ready', textColor: 'text-blue-400' },
                'configured': { text: '已配置', class: 'status-configured', textColor: 'text-green-400' },
                'not_configured': { text: '未配置', class: 'status-not-configured', textColor: 'text-gray-400' },
                'completed': { text: '已完成', class: 'status-completed', textColor: 'text-purple-400' },
                'unknown': { text: '未知', class: 'bg-gray-500', textColor: 'text-gray-400' }
            };
            return map[status] || map['unknown'];
        }

        async function startProject(id) {
            if (!confirm('确定要启动此服务吗？')) return;
            try {
                const res = await fetch(`/api/projects/${id}/start`, { method: 'POST' });
                const data = await res.json();
                alert(data.message || data.error);
                setTimeout(loadProjects, 2000);
            } catch (e) {
                alert('启动失败: ' + e.message);
            }
        }

        async function stopProject(id) {
            if (!confirm('确定要停止此服务吗？')) return;
            try {
                const res = await fetch(`/api/projects/${id}/stop`, { method: 'POST' });
                const data = await res.json();
                alert(data.message || data.error);
                setTimeout(loadProjects, 1000);
            } catch (e) {
                alert('停止失败: ' + e.message);
            }
        }

        async function restartProject(id) {
            if (!confirm('确定要重启此服务吗？')) return;
            try {
                await fetch(`/api/projects/${id}/stop`, { method: 'POST' });
                setTimeout(async () => {
                    const res = await fetch(`/api/projects/${id}/start`, { method: 'POST' });
                    const data = await res.json();
                    alert(data.message || data.error);
                    loadProjects();
                }, 2000);
            } catch (e) {
                alert('重启失败: ' + e.message);
            }
        }

        async function viewLog(id) {
            const project = projects.find(p => p.id === id);
            document.getElementById('logTitle').textContent = `${project?.name || id} - 日志`;
            document.getElementById('logModal').classList.remove('hidden');
            document.getElementById('logModal').classList.add('flex');
            
            try {
                const res = await fetch(`/api/projects/${id}/log`);
                const data = await res.json();
                document.getElementById('logContent').textContent = data.log || '暂无日志';
            } catch (e) {
                document.getElementById('logContent').textContent = '获取日志失败: ' + e.message;
            }
        }

        function closeLogModal() {
            document.getElementById('logModal').classList.add('hidden');
            document.getElementById('logModal').classList.remove('flex');
        }

        // ==================== Cron 管理 ====================

        async function loadCronTasks() {
            try {
                const res = await fetch('/api/cron/tasks');
                const data = await res.json();
                cronTasks = data.tasks;
                renderCronTasks();
                updateCronStats();
            } catch (e) {
                console.error('加载 Cron 任务失败:', e);
                document.getElementById('cronTasksList').innerHTML = '<div class="text-center py-8 text-red-400"><i class="ri-error-warning-line text-2xl"></i><p class="mt-2 text-sm">加载失败: ' + e.message + '</p></div>';
            }
        }

        function updateCronStats() {
            const total = cronTasks.length;
            const enabled = cronTasks.filter(t => t.enabled === 1).length;
            const disabled = total - enabled;
            
            document.getElementById('cronTotal').textContent = total;
            document.getElementById('cronEnabled').textContent = enabled;
            document.getElementById('cronDisabled').textContent = disabled;
            
            // 获取历史总数
            fetch('/api/cron/tasks').then(() => {
                let historyCount = 0;
                cronTasks.forEach(t => {
                    fetch(`/api/cron/tasks/${t.id}/history`).then(res => res.json()).then(data => {
                        historyCount += data.history.length;
                        document.getElementById('cronHistoryCount').textContent = historyCount;
                    });
                });
            });
        }

        function renderCronTasks() {
            const list = document.getElementById('cronTasksList');
            
            if (cronTasks.length === 0) {
                list.innerHTML = '<div class="text-center py-12 text-gray-400"><i class="ri-folder-open-line text-4xl"></i><p class="mt-2">暂无 Cron 任务</p><p class="text-sm mt-1">点击"从系统同步"导入现有任务</p></div>';
                return;
            }

            const categoryIcons = {
                'monitor': { icon: 'ri-heart-pulse-line', color: 'blue' },
                'backup': { icon: 'ri-archive-line', color: 'green' },
                'cleanup': { icon: 'ri-delete-bin-line', color: 'yellow' },
                'report': { icon: 'ri-file-chart-line', color: 'purple' },
                'sync': { icon: 'ri-refresh-line', color: 'cyan' },
                'notification': { icon: 'ri-notification-line', color: 'orange' },
                'other': { icon: 'ri-more-line', color: 'gray' }
            };

            let html = '';
            cronTasks.forEach(task => {
                const catInfo = categoryIcons[task.category] || categoryIcons['other'];
                
                html += `
                    <div class="cron-card card rounded-lg p-4">
                        <div class="flex items-start justify-between gap-4">
                            <div class="flex-1">
                                <div class="flex items-center gap-2 mb-2">
                                    <i class="${catInfo.icon} text-${catInfo.color}-400"></i>
                                    <h3 class="font-semibold">${task.name}</h3>
                                    <span class="px-2 py-0.5 rounded text-xs ${task.enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">${task.enabled ? '启用' : '禁用'}</span>
                                </div>
                                <div class="flex items-center gap-3 mb-2">
                                    <code class="text-yellow-400 text-sm font-mono">${task.expression}</code>
                                    <span class="text-gray-500">→</span>
                                    <span class="text-blue-400 text-sm">${task.human_time || ''}</span>
                                    ${task.next_run ? `<span class="text-gray-500 text-xs">下次: ${task.next_run}</span>` : ''}
                                </div>
                                <div class="text-gray-300 text-sm font-mono break-all bg-gray-800/50 rounded p-2">${escapeHtml(task.command)}</div>
                                ${task.description ? `<div class="text-gray-400 text-sm mt-2"><i class="ri-info-line"></i> ${task.description}</div>` : ''}
                            </div>
                            <div class="flex flex-col gap-1">
                                <button onclick="editCronTask(${task.id})" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-xs flex items-center gap-1"><i class="ri-edit-line"></i> 编辑</button>
                                <button onclick="toggleCronTask(${task.id})" class="btn ${task.enabled ? 'bg-yellow-600 hover:bg-yellow-700' : 'bg-green-600 hover:bg-green-700'} px-2 py-1 rounded text-xs flex items-center gap-1">
                                    <i class="ri-${task.enabled ? 'pause' : 'play'}-line"></i> ${task.enabled ? '禁用' : '启用'}
                                </button>
                                <button onclick="showHistory(${task.id}, '${task.name}')" class="btn bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-xs flex items-center gap-1"><i class="ri-history-line"></i> 历史</button>
                                <button onclick="deleteCronTask(${task.id})" class="btn bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-xs flex items-center gap-1"><i class="ri-delete-bin-line"></i> 删除</button>
                            </div>
                        </div>
                    </div>
                `;
            });
            list.innerHTML = html;
        }

        function showAddCronModal() {
            document.getElementById('cronModalTitle').textContent = '添加 Cron 任务';
            document.getElementById('cronTaskId').value = '';
            document.getElementById('cronName').value = '';
            document.getElementById('cronExpression').value = '';
            document.getElementById('cronCommand').value = '';
            document.getElementById('cronCategory').value = 'other';
            document.getElementById('cronDescription').value = '';
            document.getElementById('cronEnabled').checked = true;
            document.getElementById('cronHumanTime').textContent = '';
            document.getElementById('cronModal').classList.remove('hidden');
        }

        function editCronTask(id) {
            const task = cronTasks.find(t => t.id === id);
            if (!task) return;
            
            document.getElementById('cronModalTitle').textContent = '编辑 Cron 任务';
            document.getElementById('cronTaskId').value = id;
            document.getElementById('cronName').value = task.name;
            document.getElementById('cronExpression').value = task.expression;
            document.getElementById('cronCommand').value = task.command;
            document.getElementById('cronCategory').value = task.category;
            document.getElementById('cronDescription').value = task.description || '';
            document.getElementById('cronEnabled').checked = task.enabled === 1;
            document.getElementById('cronHumanTime').textContent = task.human_time || '';
            document.getElementById('cronModal').classList.remove('hidden');
        }

        function closeCronModal() {
            document.getElementById('cronModal').classList.add('hidden');
        }

        async function saveCronTask(event) {
            event.preventDefault();
            
            const id = document.getElementById('cronTaskId').value;
            const data = {
                name: document.getElementById('cronName').value,
                expression: document.getElementById('cronExpression').value,
                command: document.getElementById('cronCommand').value,
                category: document.getElementById('cronCategory').value,
                description: document.getElementById('cronDescription').value,
                enabled: document.getElementById('cronEnabled').checked ? 1 : 0
            };
            
            try {
                let res;
                if (id) {
                    res = await fetch(`/api/cron/tasks/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
                } else {
                    res = await fetch('/api/cron/tasks', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
                }
                
                const result = await res.json();
                if (result.error) {
                    alert('保存失败: ' + result.error);
                } else {
                    alert(result.message);
                    closeCronModal();
                    loadCronTasks();
                }
            } catch (e) {
                alert('保存失败: ' + e.message);
            }
        }

        async function toggleCronTask(id) {
            try {
                const res = await fetch(`/api/cron/tasks/${id}/toggle`, { method: 'POST' });
                const data = await res.json();
                alert(data.message);
                loadCronTasks();
            } catch (e) {
                alert('操作失败: ' + e.message);
            }
        }

        async function deleteCronTask(id) {
            if (!confirm('确定要删除此任务吗？删除后会从系统 crontab 中移除。')) return;
            
            try {
                const res = await fetch(`/api/cron/tasks/${id}`, { method: 'DELETE' });
                const data = await res.json();
                alert(data.message);
                loadCronTasks();
            } catch (e) {
                alert('删除失败: ' + e.message);
            }
        }

        async function showHistory(id, name) {
            document.getElementById('historyModalTitle').textContent = `${name} - 历史版本`;
            document.getElementById('historyModal').classList.remove('hidden');
            
            try {
                const res = await fetch(`/api/cron/tasks/${id}/history`);
                const data = await res.json();
                renderHistory(data.history, id);
            } catch (e) {
                document.getElementById('historyList').innerHTML = '<div class="text-center py-8 text-red-400">加载失败</div>';
            }
        }

        function renderHistory(history, taskId) {
            if (history.length === 0) {
                document.getElementById('historyList').innerHTML = '<div class="text-center py-8 text-gray-400">暂无历史记录</div>';
                return;
            }

            const operationLabels = {
                'create': { text: '创建', class: 'create' },
                'update': { text: '更新', class: 'update' },
                'delete': { text: '删除', class: 'delete' },
                'toggle': { text: '切换状态', class: 'toggle' },
            };

            let html = '';
            history.forEach(h => {
                const opInfo = operationLabels[h.operation] || { text: h.operation, class: 'update' };
                const isRollback = h.operation.includes('rollback');
                
                html += `
                    <div class="history-item ${isRollback ? 'rollback' : opInfo.class} py-2">
                        <div class="flex items-center justify-between">
                            <div>
                                <div class="flex items-center gap-2">
                                    <span class="text-gray-400 text-sm">v${h.version}</span>
                                    <span class="text-xs px-2 py-0.5 rounded ${isRollback ? 'bg-yellow-500/20 text-yellow-400' : 'bg-blue-500/20 text-blue-400'}">${isRollback ? '回退' : opInfo.text}</span>
                                </div>
                                <div class="text-gray-300 text-sm mt-1">${h.created_at}</div>
                                ${h.expression ? `<code class="text-yellow-400 text-xs font-mono">${h.expression}</code>` : ''}
                                ${h.enabled !== null ? `<span class="text-xs ml-2 ${h.enabled ? 'text-green-400' : 'text-red-400'}">${h.enabled ? '启用' : '禁用'}</span>` : ''}
                            </div>
                            ${!isRollback && h.version > 1 ? `<button onclick="rollbackTo(${taskId}, ${h.version})" class="btn bg-yellow-600 hover:bg-yellow-700 px-2 py-1 rounded text-xs"><i class="ri-history-line"></i> 回退</button>` : ''}
                        </div>
                    </div>
                `;
            });
            document.getElementById('historyList').innerHTML = html;
        }

        async function rollbackTo(taskId, version) {
            if (!confirm(`确定要回退到版本 ${version} 吗？`)) return;
            
            try {
                const res = await fetch(`/api/cron/tasks/${taskId}/rollback/${version}`, { method: 'POST' });
                const data = await res.json();
                alert(data.message);
                closeHistoryModal();
                loadCronTasks();
            } catch (e) {
                alert('回退失败: ' + e.message);
            }
        }

        function closeHistoryModal() {
            document.getElementById('historyModal').classList.add('hidden');
        }

        async function syncFromSystem() {
            if (!confirm('从系统 crontab 同步任务到数据库？')) return;
            
            try {
                const res = await fetch('/api/cron/sync', { method: 'POST' });
                const data = await res.json();
                alert(data.message);
                loadCronTasks();
            } catch (e) {
                alert('同步失败: ' + e.message);
            }
        }

        function toggleLogs() {
            enableLogs = document.getElementById('enableLogs').checked;
            localStorage.setItem('enableLogs', enableLogs);
        }

        // 解析 cron 表达式显示友好时间
        document.getElementById('cronExpression').addEventListener('input', function() {
            const expr = this.value;
            // 简单解析
            const patterns = {
                '0 4 * * *': '每天凌晨4点',
                '0 0 * * *': '每天午夜0点',
                '*/5 * * * *': '每5分钟',
                '*/10 * * * *': '每10分钟',
                '*/20 * * * *': '每20分钟',
                '*/30 * * * *': '每30分钟',
                '0 */1 * * *': '每小时',
                '0 */2 * * *': '每2小时',
            };
            
            document.getElementById('cronHumanTime').textContent = patterns[expr] || '';
        });

        // ==================== 通用功能 ====================

        function refreshAll() {
            if (currentTab === 'projects') {
                loadProjects();
            } else {
                loadCronTasks();
            }
        }

        // 导出配置
        async function exportConfig() {
            try {
                const response = await fetch('/api/config/export');
                const config = await response.json();
                
                // 创建下载文件
                const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `project-panel-config-${new Date().toISOString().slice(0,10)}.json`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                
                alert('配置导出成功！');
            } catch (e) {
                alert('导出失败: ' + e.message);
            }
        }

        // 导入配置
        async function importConfig(event) {
            const file = event.target.files[0];
            if (!file) return;
            
            if (!confirm('导入配置将覆盖当前所有设置，是否继续？')) {
                event.target.value = '';
                return;
            }
            
            try {
                const text = await file.text();
                const config = JSON.parse(text);
                
                const response = await fetch('/api/config/import', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(config)
                });
                
                const result = await response.json();
                
                if (result.error) {
                    alert('导入失败: ' + result.error);
                } else {
                    alert(`配置导入成功！\n项目数: ${result.projects_count}\n外部服务数: ${result.services_count}`);
                    location.reload();
                }
            } catch (e) {
                alert('导入失败: ' + e.message);
            }
            
            event.target.value = '';
        }

        function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); }
        function scrollToBottom() { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); }

        function updateConnectionStatus(ok) {
            const statusEl = document.getElementById('connectionStatus');
            if (ok) {
                statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div><span class="text-green-400 text-xs">已连接</span>';
                statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-green-500/20';
            } else {
                statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-red-400"></div><span class="text-red-400 text-xs">断开</span>';
                statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-red-500/20';
            }
        }

        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // 初始化
        loadProjects();
        loadWebSettings(); // 加载网页设置
        
        let refreshIntervalMs = 30 * 1000; // 默认值
        let refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
        
        // 加载网页设置
        async function loadWebSettings() {
            try {
                const response = await fetch('/api/web-settings');
                const settings = await response.json();
                
                if (settings.external_ip) {
                    externalIp = settings.external_ip;
                    document.getElementById('externalIp').value = settings.external_ip;
                }
                if (settings.refresh_interval) {
                    document.getElementById('refreshInterval').value = settings.refresh_interval;
                    clearInterval(refreshTimer);
                    refreshIntervalMs = settings.refresh_interval * 1000;
                    refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
                }
            } catch (e) {
                console.error('加载网页设置失败:', e);
            }
        }
        
        function updateRefreshInterval() {
            const seconds = Math.max(5, Math.min(300, parseInt(document.getElementById('refreshInterval').value) || 30));
            document.getElementById('refreshInterval').value = seconds;
            
            // 保存到服务器
            fetch('/api/web-settings', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ external_ip: externalIp, refresh_interval: seconds })
            }).catch(e => console.error('保存失败:', e));
            
            clearInterval(refreshTimer);
            refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000);
            
            // 如果当前在系统Tab且非实时模式，更新系统刷新间隔
            if (currentTab === 'system' && !document.getElementById('realtimeCheck').checked) {
                startSystemRefresh();
            }
        }

        setInterval(() => {
            fetch('/api/projects').then(() => updateConnectionStatus(true)).catch(() => updateConnectionStatus(false));
        }, 10000);

        // ==================== Docker 监控 ====================
        
        async function loadDockerData() {
            try {
                const [containersRes, imagesRes, systemRes] = await Promise.all([
                    fetch('/api/docker/containers'),
                    fetch('/api/docker/images'),
                    fetch('/api/docker/system')
                ]);
                const containers = (await containersRes.json()).containers || [];
                const images = (await imagesRes.json()).images || [];
                const system = await systemRes.json();
                renderDockerStats(containers, images, system);
                renderDockerContainers(containers);
                renderDockerImages(images);
                document.getElementById('dockerVersion').textContent = system.version || '-';
            } catch (e) {
                console.error('Docker数据加载失败:', e);
            }
        }
        
        function renderDockerStats(containers, images, system) {
            const total = containers.length;
            const running = containers.filter(c => c.state === 'running').length;
            const stopped = total - running;
            const imgTotal = images.length;
            document.getElementById('dockerContainersTotal').textContent = total;
            document.getElementById('dockerContainersRunning').textContent = running;
            document.getElementById('dockerContainersStopped').textContent = stopped;
            document.getElementById('dockerImagesTotal').textContent = imgTotal;
            document.getElementById('dockerContainerCount').textContent = `(${running}运行 / ${total}总计)`;
            document.getElementById('dockerImageCount').textContent = `(${imgTotal}个)`;
            const imagesDisk = system.diskUsage?.find(d => d.type === 'Images');
            if (imagesDisk) {
                document.getElementById('dockerDiskSize').textContent = imagesDisk.size;
                document.getElementById('dockerReclaimable').textContent = imagesDisk.reclaimable;
            }
        }
        
        function renderDockerContainers(containers) {
            const list = document.getElementById('docker-containers-grid');
            if (containers.length === 0) {
                list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无容器</div>';
                return;
            }
            list.innerHTML = containers.map(c => {
                const isRunning = c.state === 'running';
                const s = c.stats || {};
                const portsDisplay = c.ports ? c.ports.split(',').slice(0, 3).join(', ') : '';
                return `<div class="card rounded-lg p-3 ${isRunning ? 'border-green-500/30' : 'border-red-500/30'}">
                    <div class="flex items-center justify-between mb-2">
                        <div class="flex items-center gap-2">
                            <div class="w-2 h-2 rounded-full ${isRunning ? 'bg-green-400' : 'bg-red-400'}" style="${isRunning ? 'box-shadow: 0 0 8px #4ade80;' : ''}"></div>
                            <span class="font-semibold text-sm text-gray-200">${c.name}</span>
                            <span class="text-xs ${isRunning ? 'text-green-400' : 'text-red-400'}">${isRunning ? '运行中' : '已停止'}</span>
                        </div>
                        <div class="flex items-center gap-1">
                            ${isRunning ? `<button onclick="dockerControl('${c.id}','stop')" class="btn bg-red-600/50 hover:bg-red-600 px-2 py-0.5 rounded text-xs">停止</button><button onclick="dockerControl('${c.id}','restart')" class="btn bg-yellow-600/50 hover:bg-yellow-600 px-2 py-0.5 rounded text-xs">重启</button>` : `<button onclick="dockerControl('${c.id}','start')" class="btn bg-green-600/50 hover:bg-green-600 px-2 py-0.5 rounded text-xs">启动</button>`}
                            <button onclick="showDockerLogs('${c.id}','${c.name}')" class="btn bg-gray-600/50 hover:bg-gray-600 px-2 py-0.5 rounded text-xs">日志</button>
                        </div>
                    </div>
                    <div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
                        <div><span class="text-gray-500">镜像:</span> <span class="text-gray-300">${c.image}</span></div>
                        ${isRunning ? `<div><span class="text-gray-500">CPU:</span> <span class="text-blue-400">${s.cpuPercent || '-'}</span></div><div><span class="text-gray-500">内存:</span> <span class="text-green-400">${s.memUsage || '-'} (${s.memPercent || '-'})</span></div><div><span class="text-gray-500">进程:</span> <span class="text-gray-300">${s.pids || '-'}</span></div><div><span class="text-gray-500">网络I/O:</span> <span class="text-cyan-400">${s.netIO || '-'}</span></div><div><span class="text-gray-500">磁盘I/O:</span> <span class="text-purple-400">${s.blockIO || '-'}</span></div>` : ''}
                        ${portsDisplay ? `<div class="col-span-2"><span class="text-gray-500">端口:</span> <span class="text-yellow-400">${portsDisplay}</span></div>` : ''}
                        <div class="col-span-2"><span class="text-gray-500">运行时间:</span> <span class="text-gray-400">${c.runningFor || '-'}</span></div>
                    </div>
                </div>`;
            }).join('');
        }
        
        function renderDockerImages(images) {
            const list = document.getElementById('docker-images-grid');
            if (images.length === 0) {
                list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无镜像</div>';
                return;
            }
            const valid = images.filter(i => !i.dangling);
            const dangling = images.filter(i => i.dangling);
            let html = '';
            if (valid.length > 0) {
                html += valid.map(i => `<div class="flex items-center justify-between bg-gray-700/30 px-3 py-1.5 rounded text-xs"><div class="flex items-center gap-2 flex-1 min-w-0"><span class="text-gray-200 truncate">${i.repository}:${i.tag || 'latest'}</span><span class="text-gray-500">${i.size}</span></div><span class="text-gray-500 ml-2 shrink-0">${(i.created || '').substring(0, 19)}</span></div>`).join('');
            }
            if (dangling.length > 0) {
                html += `<div class="text-yellow-400/70 text-xs mt-2 mb-1"><i class="ri-error-warning-line"></i> 悬空镜像 (${dangling.length}个)</div>`;
                html += dangling.map(i => `<div class="flex items-center justify-between bg-yellow-500/10 px-3 py-1.5 rounded text-xs opacity-70"><div class="flex items-center gap-2 flex-1 min-w-0"><span class="text-gray-400 truncate">&lt;none&gt;:&lt;none&gt;</span><span class="text-gray-500">${i.size}</span><span class="text-gray-500">${i.id}</span></div><span class="text-gray-500 ml-2 shrink-0">${(i.created || '').substring(0, 19)}</span></div>`).join('');
            }
            list.innerHTML = html;
        }
        
        async function dockerControl(containerId, action) {
            const names = { start: '启动', stop: '停止', restart: '重启' };
            if (!confirm(`确定要${names[action]}容器吗？`)) return;
            try {
                const res = await fetch(`/api/docker/container/${containerId}/${action}`, { method: 'POST' });
                const data = await res.json();
                if (data.error) { alert(`操作失败: ${data.error}`); return; }
                setTimeout(loadDockerData, 1000);
            } catch (e) { alert('操作失败: ' + e.message); }
        }
        
        async function showDockerLogs(containerId, name) {
            const m = document.getElementById('logModal');
            document.getElementById('logTitle').textContent = `${name} - 日志`;
            document.getElementById('logContent').textContent = '加载中...';
            m.classList.add('flex'); m.classList.remove('hidden');
            try {
                const res = await fetch(`/api/docker/container/${containerId}/logs?lines=200`);
                const data = await res.json();
                document.getElementById('logContent').textContent = data.logs || data.error || '无日志';
            } catch (e) { document.getElementById('logContent').textContent = '加载失败: ' + e.message; }
        }
        
        async function dockerPrune(target) {
            const names = { all: '所有资源(停止的容器+悬空镜像+构建缓存)', containers: '停止的容器', images: '悬空镜像', build: '构建缓存' };
            if (!confirm(`确定清理${names[target]}？不可撤销。`)) return;
            try {
                const res = await fetch('/api/docker/prune', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({target}) });
                alert((await res.json()).message || '完成');
                setTimeout(loadDockerData, 1000);
            } catch (e) { alert('失败: ' + e.message); }
        }

        // ==================== 服务发现 ====================
        
        async function scanServices() {
            const list = document.getElementById('discoveryList');
            list.innerHTML = '<div class="text-gray-400 text-xs text-center py-2"><i class="ri-loader-4-line animate-spin"></i> 扫描中...</div>';
            
            try {
                // 收集前端已注册的自定义服务端口，传给后端排除
                const customServices = getCustomServices();
                const customPorts = customServices.map(s => parseInt(s.port)).filter(p => !isNaN(p));
                const excludeParam = customPorts.length > 0 ? `?exclude_ports=${customPorts.join(',')}` : '';
                
                const res = await fetch(`/api/discovery/services${excludeParam}`);
                const data = await res.json();
                const services = data.services || [];
                document.getElementById('discoveryCount').textContent = `发现 ${services.length} 个`;
                
                if (services.length === 0) {
                    list.innerHTML = '<div class="text-green-400 text-xs text-center py-2"><i class="ri-checkbox-circle-line"></i> 所有服务已注册，没有遗漏</div>';
                } else {
                    list.innerHTML = services.map(s => `
                        <div class="flex items-center justify-between bg-gray-700/30 px-3 py-2 rounded-lg">
                            <div class="flex items-center gap-3 flex-1 min-w-0">
                                <span class="text-xs text-cyan-400 font-mono">:${s.port}</span>
                                <span class="text-gray-300 text-sm truncate">${s.suggested_name}</span>
                                <span class="text-gray-500 text-xs">PID ${s.pid != null ? s.pid : '受权限限制'}</span>
                                <span class="text-gray-500 text-xs truncate hidden md:inline">${(s.cmdline || '').substring(0, 60)}</span>
                            </div>
                            <button onclick="quickAddService(${s.port}, '${s.suggested_name.replace(/'/g, "\\'")}', '${s.suggested_type}', '${(s.cwd || '').replace(/'/g, "\\'")}')" class="btn bg-green-600/50 hover:bg-green-600 px-2 py-1 rounded text-xs shrink-0 ml-2">
                                <i class="ri-add-line"></i> 添加
                            </button>
                        </div>
                    `).join('');
                }
            } catch (e) {
                list.innerHTML = `<div class="text-red-400 text-xs text-center py-2">扫描失败: ${e.message}</div>`;
            }
        }
        
        function quickAddService(port, name, type, cwd) {
            showAddServiceModal();
            document.getElementById('serviceName').value = name;
            document.getElementById('servicePort').value = port;
            document.getElementById('serviceFullUrl').value = `http://${externalIp}:${port}`;
            document.getElementById('serviceDesc').value = `自动发现 | ${type} | PID路径: ${cwd || '未知'}`;
            updateServicePreview();
        }
        
        // 页面加载后显示发现区块
        function showDiscoverySection() {
            document.getElementById('discoverySection').classList.remove('hidden');
        }
        setTimeout(showDiscoverySection, 500);

    </script>
</body>
</html>