Files
Nexus/web/app/install.html
T
Nexus Deploy 5e7c5beb43 fix(install): probe redis via docker service host in env-check
Install wizard no longer hardcodes 127.0.0.1:6379; Docker installs get
mysql/redis prefills and a passing Redis check inside the nexus container.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:31:11 +08:00

737 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" x-data="installWizard()" x-init="init()">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus 6.0 安装向导</title>
<script src="/app/vendor/tailwindcss-browser.js"></script>
<script src="/app/vendor/theme-init.js"></script>
<link rel="stylesheet" href="/app/vendor/theme.css">
<script defer src="/app/vendor/alpinejs.min.js"></script>
<style type="text/tailwindcss">
@theme {
--color-brand: oklch(55% 0.2 250);
--color-brand-light: oklch(75% 0.15 250);
--color-brand-dark: oklch(35% 0.18 250);
}
</style>
<style>
.installer-card { max-width: 780px; }
.code-block {
background: #1e293b; color: #e2e8f0; border-radius: 6px; padding: 12px;
font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all;
position: relative; margin: 8px 0;
}
.copy-btn {
position: absolute; top: 6px; right: 6px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
color: #94a3b8; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
}
.copy-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
.copy-btn.copied { background: #22c55e; color: #fff; border-color: #22c55e; }
</style>
</head>
<body class="bg-[var(--bg-page)] min-h-screen flex items-center justify-center p-4">
<div class="installer-card w-full bg-white rounded-2xl shadow-2xl overflow-hidden">
<!-- Header -->
<div class="bg-gradient-to-br from-blue-700 to-violet-600 text-white px-8 py-6 text-center">
<h1 class="text-2xl font-bold mb-1">Nexus 6.0 安装向导</h1>
<p class="text-violet-200 text-sm">服务器运维管理平台 — 心跳监控 + 智能告警 + 3层守护</p>
</div>
<!-- Step Indicator -->
<div class="flex items-center justify-center gap-1 py-4 px-8">
<template x-for="i in 5" :key="i">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all"
:class="step > i ? 'bg-green-500 text-white' : step === i ? 'bg-blue-500 text-white' : 'border-2 border-slate-200 text-[var(--text-secondary)]'"
x-text="step > i ? '✓' : i"></div>
<div x-show="i < 5" class="w-8 h-0.5 mx-0.5 transition-all"
:class="step > i ? 'bg-green-500' : 'bg-slate-200'"></div>
</div>
</template>
</div>
<!-- Body -->
<div class="px-8 pb-8">
<!-- Error / Success alerts -->
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-sm" x-text="error"></div>
<div x-show="success" x-transition class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-4 text-sm" x-text="success"></div>
<!-- Already installed -->
<template x-if="installed">
<div class="text-center py-8">
<div class="text-5xl mb-4">⚠️</div>
<h2 class="text-xl font-bold text-red-600 mb-2">系统已安装</h2>
<p class="text-[var(--text-muted)] mb-4">检测到 .env 配置文件,系统已完成安装。</p>
<a href="/app/login.html" class="inline-flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-5 py-2.5 rounded-lg font-medium transition">前往登录 →</a>
</div>
</template>
<!-- ======== Step 1: Welcome ======== -->
<template x-if="step === 1 && !installed">
<div class="text-center py-4">
<h2 class="text-xl font-bold text-slate-800 mb-4">欢迎使用 Nexus 6.0</h2>
<p class="text-[var(--text-muted)] mb-6 leading-relaxed">
本向导将引导您完成系统安装配置。<br>
安装完成后将自动生成:<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">.env</code> (Python后端) +
<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">config.json</code> (共享配置) +
<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">MySQL settings表</code> (共享配置)<br>
整个过程大约 3 分钟。
</p>
<div class="bg-slate-50 rounded-lg p-5 text-left mb-6">
<h3 class="font-semibold text-slate-700 mb-3">📋 安装前准备</h3>
<ul class="text-[var(--text-dim)] text-sm ml-5 list-disc space-y-1">
<li>Python 3.12+ (FastAPI + uvicorn)</li>
<li>MySQL 8.0+ (需提前创建数据库和用户)</li>
<li>Redis 6+ (心跳缓冲和实时数据)</li>
<li>Web 目录和根目录可写权限</li>
</ul>
</div>
<button @click="step = 2; checkEnv()" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2.5 rounded-lg font-semibold transition">
开始安装 →
</button>
</div>
</template>
<!-- ======== Step 2: Environment Check ======== -->
<template x-if="step === 2 && !installed">
<div>
<h2 class="text-lg font-bold text-slate-800 mb-4">🔍 环境检测</h2>
<div x-show="envLoading" class="text-center py-8 text-[var(--text-secondary)]">
<div class="animate-spin inline-block w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mb-2"></div>
<p>正在检测环境...</p>
</div>
<div x-show="!envLoading && envChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-4">
<template x-for="c in envChecks" :key="c.name">
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0">
<div>
<span class="font-medium text-slate-700" x-text="c.name"></span>
<span class="text-[var(--text-secondary)] text-xs ml-2" x-text="'需要: ' + c.required"></span>
</div>
<span class="font-semibold text-sm" :class="c.pass ? 'text-green-500' : 'text-red-500'"
x-text="c.pass ? '✓ 通过' : '✗ ' + c.current"></span>
</div>
</template>
</div>
<div x-show="!envLoading && envAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-4 text-sm">
✓ 所有环境检测通过,可以继续安装。
</div>
<div x-show="!envLoading && !envAllPass && envChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-sm">
部分环境检测未通过,请先解决上述问题。
</div>
<div x-show="!envLoading" class="flex gap-3 mt-4">
<button @click="step = 3" class="bg-blue-500 hover:bg-blue-600 text-white px-5 py-2.5 rounded-lg font-semibold transition">
下一步:数据库配置 →
</button>
<button @click="checkEnv()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">
重新检测
</button>
</div>
</div>
</template>
<!-- ======== Step 3: DB + Redis + API Config ======== -->
<template x-if="step === 3 && !installed">
<div>
<h2 class="text-lg font-bold text-slate-800 mb-4">⚙ 数据库 + Redis + API 配置</h2>
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg mb-4 text-sm">
<b>说明:</b>请提前创建好数据库和用户,安装向导将自动建表并写入配置。
SECRET_KEY 和 API_KEY 将自动生成,安装后不可修改(加密一致性)。
</div>
<form @submit.prevent="initDb()">
<!-- MySQL -->
<div class="border-t-2 border-slate-100 pt-4 mt-4">
<h3 class="font-bold text-slate-700 mb-3">🗄 MySQL 数据库</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">主机</label>
<input x-model="form.db_host" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">端口</label>
<input x-model="form.db_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-3">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">数据库名</label>
<input x-model="form.db_name" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">用户名</label>
<input x-model="form.db_user" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">密码</label>
<input x-model="form.db_pass" type="password" required placeholder="数据库密码"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<!-- Redis -->
<div class="border-t-2 border-slate-100 pt-4 mt-4">
<h3 class="font-bold text-slate-700 mb-3">⚡ Redis 配置</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 主机</label>
<input x-model="form.redis_host" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 端口</label>
<input x-model="form.redis_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-3">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
<input x-model="form.redis_db" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码(可选)</label>
<input x-model="form.redis_pass" type="password" placeholder="无密码留空"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
</div>
<!-- API + Site + Timezone -->
<div class="border-t-2 border-slate-100 pt-4 mt-4">
<h3 class="font-bold text-slate-700 mb-3">🌐 网站地址 + API 服务 + 时区</h3>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">网站地址</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">自动检测 — 用于 Agent 上报和前端 API 调用,可手动修改</p>
<input x-model="form.site_url" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div class="grid grid-cols-2 gap-4 mt-3">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Python API 端口</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">uvicorn 监听端口</p>
<input x-model="form.api_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">时区</label>
<select x-model="form.timezone" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
<option value="Asia/Shanghai">Asia/Shanghai (中国标准时间)</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
<option value="UTC">UTC</option>
</select>
</div>
</div>
</div>
<div class="bg-amber-50 border border-amber-200 text-amber-800 px-4 py-3 rounded-lg mt-4 text-sm">
<b>⚠ 注意:</b>初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY
③ 写入 .env + config.json + settings表 ④ 自动计算连接池大小
</div>
<div class="flex gap-3 mt-5">
<button type="submit" :disabled="loading" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
<span x-text="loading ? '初始化中...' : '初始化数据库 →'"></span>
</button>
<button type="button" @click="step = 2" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
</div>
</form>
</div>
</template>
<!-- ======== Step 4: Admin + Brand ======== -->
<template x-if="step === 4 && !installed">
<div>
<h2 class="text-lg font-bold text-slate-800 mb-4">👤 管理员账号 + 系统名称</h2>
<!-- Step 3 summary -->
<div class="bg-slate-50 rounded-lg p-4 mb-4 text-sm">
<div class="font-semibold text-green-600 mb-2">✓ 步骤 3 配置完成</div>
<div class="grid grid-cols-2 gap-1">
<span>数据库: <code class="bg-slate-200 px-1 rounded" x-text="form.db_name"></code></span>
<span>用户: <code class="bg-slate-200 px-1 rounded" x-text="form.db_user"></code></span>
<span>连接池: <code class="bg-slate-200 px-1 rounded" x-text="initResult?.pool_size || '—'"></code></span>
<span>溢出池: <code class="bg-slate-200 px-1 rounded" x-text="initResult?.max_overflow || '—'"></code></span>
</div>
</div>
<form @submit.prevent="createAdmin()">
<!-- Brand -->
<div class="border-t-2 border-slate-100 pt-4">
<h3 class="font-bold text-slate-700 mb-3">系统品牌</h3>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">系统名称</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">显示在浏览器标题和后台标题栏</p>
<input x-model="adminForm.system_name" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">系统标题</label>
<input x-model="adminForm.system_title" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<!-- Admin account -->
<div class="border-t-2 border-slate-100 pt-4 mt-4">
<h3 class="font-bold text-slate-700 mb-3">管理员账号</h3>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">管理员用户名</label>
<input x-model="adminForm.admin_username" required class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">管理员密码</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">至少6位字符</p>
<input x-model="adminForm.admin_password" type="password" required minlength="6" placeholder="请输入安全密码"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">邮箱(可选)</label>
<input x-model="adminForm.admin_email" type="email" placeholder="admin@example.com"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<div class="flex gap-3 mt-5">
<button type="submit" :disabled="loading" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
<span x-text="loading ? '创建中...' : '创建账号 →'"></span>
</button>
<button type="button" @click="step = 3" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
</div>
</form>
</div>
</template>
<!-- ======== Step 5: Complete ======== -->
<template x-if="step === 5 && !installed">
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-green-100 text-green-500 text-3xl flex items-center justify-center mx-auto mb-4"></div>
<h2 class="text-xl font-bold text-slate-800 mb-2">安装完成!</h2>
<p class="text-[var(--text-muted)] mb-1">Nexus 6.0 已成功安装。</p>
<p class="text-[var(--text-secondary)] text-xs">安装向导已锁定 — 防止重复安装(如需重装请删除 .env 文件后重启服务)</p>
<!-- Guardian results -->
<div x-show="initResult?.guardian_results?.length" class="text-left mt-6 mb-4 rounded-lg p-4"
:class="guardianAllOk ? 'bg-green-50 border-l-4 border-green-500' : 'bg-amber-50 border-l-4 border-amber-500'">
<div class="font-semibold text-sm mb-2" :class="guardianAllOk ? 'text-green-700' : 'text-amber-700'"
x-text="guardianAllOk ? '✓ 进程守护已自动配置' : '⚠ 进程守护部分配置失败'"></div>
<template x-for="r in (initResult?.guardian_results || [])" :key="r">
<div class="text-xs leading-relaxed" x-text="r"></div>
</template>
</div>
<!-- Post-install checklist -->
<div class="text-left mt-6 space-y-3">
<h3 class="text-sm font-bold text-slate-700 text-center mb-3">━━━ 后续配置清单(必须完成)━━━</h3>
<!-- 1. Supervisor -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">1</span>
Supervisor 进程守护
</div>
<div x-show="!guardianAllOk" class="text-xs text-[var(--text-muted)] space-y-1">
<!-- BT Panel mode -->
<template x-if="btPanel">
<div>
<p>宝塔面板 → 软件商店 → Supervisor管理器 → 添加守护进程</p>
<p>或手动写入配置: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/www/server/panel/plugin/supervisor/profile/nexus.ini</code></p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'supervisorConf')">复制配置</button><pre id="supervisorConf">[program:nexus]
command=<span x-text="installDir"></span>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <span x-text="form.api_port"></span>
directory=<span x-text="installDir"></span>
user=root
autostart=true
autorestart=true
startretries=10
startsecs=3
stopwaitsecs=10
stopsignal=INT
environment=PATH="<span x-text="installDir"></span>/venv/bin:%(ENV_PATH)s"
stderr_logfile=/www/wwwlogs/nexus_error.log
stdout_logfile=/www/wwwlogs/nexus_access.log
stderr_logfile_maxbytes=50MB
stdout_logfile_maxbytes=50MB</pre></div>
<p>重载: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">supervisorctl reread && supervisorctl update && supervisorctl start nexus</code></p>
</div>
</template>
<!-- Standard mode -->
<template x-if="!btPanel">
<div>
<p>安装: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo apt install -y supervisor</code></p>
<p>配置文件复制到: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/etc/supervisor/conf.d/nexus.conf</code></p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'supervisorConf2')">复制配置</button><pre id="supervisorConf2">[program:nexus]
command=<span x-text="installDir"></span>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <span x-text="form.api_port"></span>
directory=<span x-text="installDir"></span>
user=root
autostart=true
autorestart=true
startretries=10
startsecs=3
stopwaitsecs=10
stopsignal=INT
environment=PATH="<span x-text="installDir"></span>/venv/bin:%(ENV_PATH)s"
stderr_logfile=/var/log/nexus/error.log
stdout_logfile=/var/log/nexus/access.log
stderr_logfile_maxbytes=50MB
stdout_logfile_maxbytes=50MB</pre></div>
<p>启动: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus</code></p>
</div>
</template>
</div>
<div x-show="guardianAllOk" class="text-xs text-green-600">✓ 已自动配置 — Supervisor 配置已写入,服务已重载</div>
</div>
<!-- 2. Python venv -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">2</span>
Python 虚拟环境 + 依赖
</div>
<div class="text-xs text-[var(--text-muted)] space-y-1">
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">cd <span x-text="installDir"></span></code></p>
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">python3.12 -m venv venv</code></p>
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">source venv/bin/activate</code></p>
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">pip install -r requirements.txt</code></p>
</div>
</div>
<!-- 3. Nginx -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">3</span>
Nginx 反向代理 (API + WebSocket)
</div>
<div class="text-xs text-[var(--text-muted)]">
<!-- BT Panel mode -->
<template x-if="btPanel">
<div>
<p class="mb-1">宝塔 → 网站 → 配置文件 → <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">#PHP-INFO-END</code> 后面粘贴:</p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxConf')">复制配置</button><pre id="nginxConf"> # Nexus Python API + WebSocket
location ^~ /api/ {
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /health {
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
}
location ^~ /ws/ {
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}</pre></div>
</div>
</template>
<!-- Standard mode -->
<template x-if="!btPanel">
<div>
<p class="mb-1">将以下配置写入 <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/etc/nginx/sites-available/nexus</code> 并创建符号链接到 sites-enabled</p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxConf2')">复制配置</button><pre id="nginxConf2">upstream nexus_api {
server 127.0.0.1:<span x-text="form.api_port"></span>;
keepalive 32;
}
server {
listen 80;
server_name YOUR_DOMAIN;
location / {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
location /ws/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
location ~ ^/(\.env|\.git) {
return 404;
}
}</pre></div>
<p>启用: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx</code></p>
</div>
</template>
</div>
</div>
<!-- 4. Nginx Rewrite -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">4</span>
Nginx 伪静态
</div>
<div class="text-xs text-[var(--text-muted)]">
<p class="mb-1">宝塔 → 网站 → 伪静态 → 粘贴:</p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxRewrite')">复制</button><pre id="nginxRewrite">location /app/ {
try_files $uri $uri/ /app/index.html;
}
location / {
try_files $uri $uri/ =404;
}</pre></div>
</div>
</div>
<!-- 5. SSL -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">5</span>
SSL证书 (强制HTTPS)
</div>
<div class="text-xs text-[var(--text-muted)]">
<template x-if="btPanel">
<div>宝塔 → 网站 → SSL → Let's Encrypt → 一键申请 → 开启强制HTTPS</div>
</template>
<template x-if="!btPanel">
<div>安装certbot: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo apt install -y certbot python3-certbot-nginx</code><br>
申请证书: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo certbot --nginx -d YOUR_DOMAIN</code></div>
</template>
</div>
</div>
<!-- 6. Health Monitor -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">6</span>
Shell 健康检查 (外部守护 + Telegram告警)
</div>
<div x-show="!guardianAllOk" class="text-xs text-[var(--text-muted)]">
<p>配置 crontab:</p>
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'crontab')">复制</button><pre id="crontab">* * * * * <span x-text="installDir"></span>/deploy/health_monitor.sh</pre></div>
</div>
<div x-show="guardianAllOk" class="text-xs text-green-600">✓ 已自动配置 — health_monitor.sh INSTALL_DIR 已更新,Crontab 已添加</div>
<div class="text-xs text-[var(--text-muted)] mt-2">
<b>3层守护机制:</b><br>
Layer 1: Supervisor — Python崩溃自动重启<br>
Layer 2: Python self_monitor — 每30s检查Redis/MySQL/WebSocket<br>
Layer 3: Shell脚本 — 每分钟HTTP检查,连续3次失败→Supervisor重启+Telegram告警
</div>
</div>
<!-- 7. Security -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div class="font-bold text-sm text-slate-800 mb-2">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">7</span>
安全收尾
</div>
<div class="text-xs text-[var(--text-muted)] space-y-1">
<p>✓ 安装向导已锁定 — 删除 <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">.env</code> 文件可重新安装</p>
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">.env</code> 包含 SECRET_KEY/API_KEY — <b>安装后不可修改</b>(加密一致性)</p>
<p>✓ 防火墙限制端口 <span x-text="form.api_port"></span> (仅允许本机和子服务器IP)</p>
<p>✓ 修改管理员默认密码</p>
</div>
</div>
</div>
<div class="flex justify-center mt-6">
<a href="/app/index.html" class="bg-green-500 hover:bg-green-600 text-white px-8 py-3 rounded-lg font-semibold text-lg transition">
进入管理面板 →
</a>
</div>
</div>
</template>
</div>
</div>
<script>
function installWizard() {
const API = window.location.origin;
return {
step: 1,
loading: false,
installed: false,
error: '',
success: '',
envLoading: false,
envChecks: [],
envAllPass: false,
initResult: null,
installToken: '',
installDir: '/opt/nexus',
btPanel: false,
form: {
db_host: 'localhost',
db_port: '3306',
db_name: 'Nexus',
db_user: 'Nexus',
db_pass: '',
redis_host: '127.0.0.1',
redis_port: '6379',
redis_db: '0',
redis_pass: '',
api_port: '8600',
timezone: 'Asia/Shanghai',
site_url: '',
},
adminForm: {
admin_username: 'admin',
admin_password: '',
admin_email: '',
system_name: 'Nexus',
system_title: 'Nexus — 服务器运维管理平台',
},
get guardianAllOk() {
const results = this.initResult?.guardian_results || [];
if (!results.length) return false;
return results.every(r => !r.includes('✗') && !r.includes('⚠'));
},
async init() {
// Auto-detect site URL
const proto = location.protocol === 'https:' ? 'https' : 'http';
this.form.site_url = proto + '://' + location.host;
// Check install status
try {
const r = await fetch(API + '/api/install/status');
if (r.ok) {
const d = await r.json();
this.installed = d.installed;
if (d.installed) return;
}
} catch(e) {}
// Try to restore state
try {
const r = await fetch(API + '/api/install/state');
if (r.ok) {
const d = await r.json();
if (d.step > 1) this.step = d.step;
if (d.db_host) Object.assign(this.form, {
db_host: d.db_host, db_port: d.db_port, db_name: d.db_name,
db_user: d.db_user, db_pass: d.db_pass, api_port: d.api_port || '8600',
});
if (d.install_dir) this.installDir = d.install_dir;
if (d.pool_size) this.initResult = { pool_size: d.pool_size, max_overflow: d.max_overflow, guardian_results: d.guardian_results || [] };
}
} catch(e) {}
},
async checkEnv() {
this.envLoading = true;
this.error = '';
try {
const r = await fetch(API + '/api/install/env-check');
if (r.ok) {
const d = await r.json();
this.envChecks = d.checks;
this.envAllPass = d.all_pass;
if (d.docker_defaults) {
Object.assign(this.form, d.docker_defaults);
}
}
} catch(e) {
this.error = '环境检测请求失败: ' + e.message;
}
this.envLoading = false;
},
async initDb() {
this.loading = true;
this.error = '';
this.success = '';
try {
const r = await fetch(API + '/api/install/init-db', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.form),
});
const d = await r.json();
if (r.ok && d.success) {
this.success = '数据库初始化成功!已创建 ' + (d.tables_created || 14) + ' 张表。';
this.initResult = d;
this.installToken = d.install_token || '';
this.btPanel = !!d.bt_panel;
this.step = 4;
} else {
this.error = d.detail || '初始化失败';
}
} catch(e) {
this.error = '请求失败: ' + e.message;
}
this.loading = false;
},
async createAdmin() {
this.loading = true;
this.error = '';
try {
const body = {
install_token: this.installToken,
...this.adminForm,
db_host: this.form.db_host,
db_port: this.form.db_port,
db_name: this.form.db_name,
db_user: this.form.db_user,
db_pass: this.form.db_pass,
};
const r = await fetch(API + '/api/install/create-admin', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
const d = await r.json();
if (r.ok && d.success) {
if (d.locked) {
this.step = 5;
this.success = '安装完成!请使用刚创建的管理员账户登录。';
return;
}
const lr = await fetch(API + '/api/install/lock', { method: 'POST' });
let ld = {};
try { ld = await lr.json(); } catch (_) {}
if (lr.ok || ld.already_locked) {
this.step = 5;
this.success = '安装完成!请使用刚创建的管理员账户登录。';
} else {
this.error = (ld.detail || '管理员已创建,但锁定安装向导失败,请再次点击完成或刷新后重试。');
}
} else {
this.error = d.detail || '创建管理员失败';
}
} catch(e) {
this.error = '请求失败: ' + e.message;
}
this.loading = false;
},
copyCode(event, id) {
const el = document.getElementById(id);
if (!el) return;
const text = el.textContent;
const btn = event.target;
navigator.clipboard.writeText(text).then(() => {
btn.textContent = '已复制 ✓';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000);
});
},
};
}
</script>
</body>
</html>