feat: complete frontend interactive features + unified sidebar

- credentials: add credential CRUD form (type, host, port, user, password, db)
- schedules: add enable/disable toggle, edit modal, server selection
- retries: add status badges, retry progress bar, error display, refresh
- audit: add action filter dropdown, IP column, better status badges
- settings: reorganize into sections (brand/alert/infra/telegram)
- servers: add/edit modal with full form fields
- scripts: add execute modal + delete button
- All pages: unified sidebar with all nav items + hover states
- install.php: fix post-AdminLTE removal link paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-22 00:02:46 +08:00
parent 00a6491e59
commit f0c586a345
10 changed files with 478 additions and 62 deletions
+50 -10
View File
@@ -1,23 +1,63 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 审计日志</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><a href="/app/index.html" class="text-white font-bold text-lg">Nexus</a></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/audit.html" class="block px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">📋 审计日志</a><a href="/app/settings.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">审计日志</h1></div></header>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
<div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div>
<nav class="flex-1 py-3 px-3 space-y-1">
<a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a>
<a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a>
<a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a>
<a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a>
<a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a>
<a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a>
<a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试队列</a>
<a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📋 审计</a>
<a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a>
</nav>
<div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">审计日志</h1></div>
<div class="flex items-center gap-3">
<select id="actionFilter" onchange="loadAudit()" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
<option value="">全部操作</option>
<option value="login">登录</option>
<option value="push">推送</option>
<option value="script_exec">脚本执行</option>
<option value="server_create">添加服务器</option>
<option value="server_delete">删除服务器</option>
<option value="setting_update">修改设置</option>
</select>
<button onclick="loadAudit()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-white text-sm rounded-lg transition">刷新</button>
</div>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="auditTable" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<table class="w-full text-sm"><thead class="bg-slate-800/50 text-slate-400 text-xs"><tr><th class="text-left px-4 py-3">操作人</th><th class="text-left px-4 py-3">操作</th><th class="text-left px-4 py-3">目标</th><th class="text-left px-4 py-3">详情</th><th class="text-right px-4 py-3">时间</th></tr></thead><tbody id="auditTbody" class="divide-y divide-slate-800"><tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">加载中...</td></tr></tbody></table>
<table class="w-full text-sm"><thead class="bg-slate-800/50 text-slate-400 text-xs uppercase"><tr><th class="text-left px-4 py-3">操作人</th><th class="text-left px-4 py-3">操作</th><th class="text-left px-4 py-3">目标</th><th class="text-left px-4 py-3">详情</th><th class="text-left px-4 py-3">IP</th><th class="text-right px-4 py-3">时间</th></tr></thead><tbody id="auditTbody" class="divide-y divide-slate-800"><tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">加载中...</td></tr></tbody></table>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
async function loadAudit(){
const r=await apiFetch(API+'/audit/?limit=100');if(!r)return;
const action=document.getElementById('actionFilter').value;
const url=API+'/audit/?limit=200'+(action?'&action='+encodeURIComponent(action):'');
const r=await apiFetch(url);if(!r)return;
const logs=await r.json();const tbody=document.getElementById('auditTbody');
if(!logs.length){tbody.innerHTML='<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">暂无日志</td></tr>';return}
tbody.innerHTML=logs.map(l=>`<tr class="hover:bg-slate-800/30"><td class="px-4 py-3 text-white">${esc(l.admin_username||'--')}</td><td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs ${l.action.includes('login')?'bg-green-400/10 text-green-400':l.action.includes('delete')?'bg-red-400/10 text-red-400':'bg-slate-800 text-slate-300'}">${esc(l.action)}</span></td><td class="px-4 py-3 text-slate-400">${esc(l.target_type||'--')}</td><td class="px-4 py-3 text-slate-500 text-xs max-w-xs truncate">${esc((l.detail||'').substring(0,80))}</td><td class="px-4 py-3 text-slate-500 text-xs text-right">${fmt(l.created_at)}</td></tr>`).join('')
if(!logs.length){tbody.innerHTML='<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">暂无日志</td></tr>';return}
tbody.innerHTML=logs.map(l=>{
const badge=l.action.includes('login')?'bg-green-400/10 text-green-400':l.action.includes('delete')?'bg-red-400/10 text-red-400':l.action.includes('create')?'bg-blue-400/10 text-blue-400':'bg-slate-800 text-slate-300';
return `<tr class="hover:bg-slate-800/30 transition">
<td class="px-4 py-3 text-white">${esc(l.admin_username||'--')}</td>
<td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs ${badge}">${esc(l.action)}</span></td>
<td class="px-4 py-3 text-slate-400 text-xs">${esc(l.target_type||'--')} ${l.target_id?'#'+l.target_id:''}</td>
<td class="px-4 py-3 text-slate-500 text-xs max-w-xs truncate" title="${esc(l.detail||'')}">${esc((l.detail||'').substring(0,80))}</td>
<td class="px-4 py-3 text-slate-600 text-xs">${esc(l.ip_address||'--')}</td>
<td class="px-4 py-3 text-slate-500 text-xs text-right whitespace-nowrap">${fmt(l.created_at)}</td>
</tr>`}).join('')
}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
function fmt(t){if(!t)return'--';return new Date(t+'Z').toLocaleString('zh-CN')}
loadAudit()
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmt(t){if(!t)return'--';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'})}
loadAudit();
</script>
</body></html>
</body></html>
+99 -7
View File
@@ -1,13 +1,105 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 凭据管理</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><span class="text-white font-bold text-lg">Nexus</span></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">🔑 凭据</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><h1 class="text-white font-semibold">凭据管理</h1></header>
<main class="flex-1 overflow-y-auto p-6"><div id="credsList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div></main>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
<div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div>
<nav class="flex-1 py-3 px-3 space-y-1">
<a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a>
<a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a>
<a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a>
<a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a>
<a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">🔑 凭据</a>
<a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a>
</nav>
<div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">凭据管理</h1></div>
<button onclick="showAddCred()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">+ 新建凭据</button>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="credsList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
<!-- Add Credential Modal -->
<div id="credModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md space-y-3">
<h2 class="text-white font-semibold text-lg">新建数据库凭据</h2>
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="credName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="prod-mysql"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-slate-400 text-xs mb-1">类型</label><select id="credType" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option value="mysql">MySQL</option><option value="postgresql">PostgreSQL</option><option value="mariadb">MariaDB</option><option value="mongodb">MongoDB</option><option value="redis">Redis</option></select></div>
<div><label class="block text-slate-400 text-xs mb-1">端口</label><input id="credPort" type="number" value="3306" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
</div>
<div><label class="block text-slate-400 text-xs mb-1">主机</label><input id="credHost" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="192.168.1.10"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-slate-400 text-xs mb-1">用户名</label><input id="credUser" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="root"></div>
<div><label class="block text-slate-400 text-xs mb-1">密码</label><input id="credPass" type="password" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
</div>
<div><label class="block text-slate-400 text-xs mb-1">数据库</label><input id="credDb" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="可选"></div>
<div class="flex gap-2 pt-2"><button onclick="createCred()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideCredModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
</div>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
async function loadCreds(){const r=await apiFetch(API+'/presets/');if(!r)return;const p=await r.json();document.getElementById('credsList').innerHTML=p.length?p.map(c=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><span class="text-white">${esc(c.name)}</span><span class="text-slate-500 text-xs ml-2">#${c.id}</span></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无凭据</div>'}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
loadCreds()
const DB_ICONS={mysql:'🐬',postgresql:'🐘',mariadb:'🐬',mongodb:'🍃',redis:'🔴'};
const DB_PORTS={mysql:3306,postgresql:5432,mariadb:3306,mongodb:27017,redis:6379};
async function loadCreds(){
try{
const r=await apiFetch(API+'/scripts/credentials');
if(!r)return;
const creds=await r.json();
document.getElementById('credsList').innerHTML=creds.length?creds.map(c=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-lg">${DB_ICONS[c.db_type]||'🗄'}</span>
<div>
<span class="text-white font-medium">${esc(c.name)}</span>
<div class="text-slate-500 text-xs mt-0.5">${esc(c.db_type)} · ${esc(c.host)}:${c.port} · ${esc(c.username)}${c.database?' / '+esc(c.database):''}</div>
</div>
</div>
<button onclick="deleteCred(${c.id})" class="text-red-400 text-xs hover:underline">删除</button>
</div>
</div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无凭据</div>';
}catch(e){document.getElementById('credsList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
}
function showAddCred(){
['credName','credHost','credUser','credPass','credDb'].forEach(id=>document.getElementById(id).value='');
document.getElementById('credType').value='mysql';
document.getElementById('credPort').value='3306';
document.getElementById('credModal').classList.remove('hidden');
}
function hideCredModal(){document.getElementById('credModal').classList.add('hidden')}
document.getElementById('credType').addEventListener('change',()=>{
const t=document.getElementById('credType').value;
document.getElementById('credPort').value=DB_PORTS[t]||3306;
});
async function createCred(){
const body={
name:document.getElementById('credName').value,
db_type:document.getElementById('credType').value,
host:document.getElementById('credHost').value,
port:parseInt(document.getElementById('credPort').value)||3306,
username:document.getElementById('credUser').value,
password:document.getElementById('credPass').value,
database:document.getElementById('credDb').value||null,
};
if(!body.name||!body.host||!body.username||!body.password){alert('名称、主机、用户名、密码必填');return}
await apiFetch(API+'/scripts/credentials',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
hideCredModal();loadCreds();
}
async function deleteCred(id){
if(!confirm('确定删除凭据?'))return;
await apiFetch(API+'/scripts/credentials/'+id,{method:'DELETE'});
loadCreds();
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
loadCreds();
</script>
</body></html>
</body></html>
+1 -1
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 文件管理</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">🖥 服务器</a><a href="/app/files.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📁 文件管理</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">📜 脚本库</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a><a href="/app/files.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📁 文件管理</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a><a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a><a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a><a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试队列</a><a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">文件管理</h1></div><div class="flex items-center gap-3"><select id="serverSelect" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option>-- 选择服务器 --</option></select><input id="dirPath" placeholder="/www/wwwroot/" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm w-64 placeholder-slate-500"><button onclick="browseDir()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">浏览</button></div></header>
<main class="flex-1 overflow-y-auto p-6"><div id="fileList" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"><div class="px-4 py-8 text-center text-slate-500">选择服务器并输入目录路径</div></div></main>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 推送</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">🖥 服务器</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">📜 脚本库</a><a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">⏰ 调度</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm transition">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a><a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a><a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a><a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试队列</a><a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">批量推送</h1></div></header>
<main class="flex-1 overflow-y-auto p-6 max-w-2xl mx-auto w-full">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 space-y-4">
+49 -7
View File
@@ -1,13 +1,55 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 重试队列</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><a href="/app/index.html" class="text-white font-bold text-lg">Nexus</a></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/push.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">📤 推送</a><a href="/app/schedules.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">⏰ 调度</a><a href="/app/retries.html" class="block px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">🔄 重试队列</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><h1 class="text-white font-semibold">重试队列</h1></header>
<main class="flex-1 overflow-y-auto p-6"><div id="retryList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div></main>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
<div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div>
<nav class="flex-1 py-3 px-3 space-y-1">
<a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a>
<a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a>
<a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a>
<a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a>
<a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a>
<a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a>
<a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">🔄 重试队列</a>
<a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a>
<a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a>
</nav>
<div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">重试队列</h1></div>
<button onclick="loadRetries()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-white text-sm rounded-lg transition">刷新</button>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="retryList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
async function loadRetries(){const r=await apiFetch(API+'/retries/');if(!r)return;const j=await r.json();document.getElementById('retryList').innerHTML=j.length?j.map(j=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><div class="flex items-center justify-between"><span class="text-white">${esc(j.server_name||'#'+j.server_id)}</span><span class="text-xs text-slate-400">重试 ${j.retry_count}/${j.max_retries}</span></div><div class="mt-1 text-xs text-slate-500">${esc(j.source_path)}${esc(j.target_path)}</div></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无重试任务</div>'}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
loadRetries()
async function loadRetries(){
try{
const r=await apiFetch(API+'/retries/');if(!r)return;
const jobs=await r.json();
document.getElementById('retryList').innerHTML=jobs.length?jobs.map(j=>{
const statusColor=j.status==='pending'?'text-yellow-400':j.status==='running'?'text-blue-400':j.status==='failed'?'text-red-400':'text-green-400';
const retryPct=Math.min((j.retry_count/j.max_retries)*100,100);
return `<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-white font-medium">${esc(j.server_name||'#'+j.server_id)}</span>
<span class="${statusColor} text-xs">${esc(j.status)}</span>
</div>
<span class="text-xs text-slate-400">重试 ${j.retry_count}/${j.max_retries}</span>
</div>
<div class="mt-2 text-xs text-slate-500">${esc(j.source_path)}${esc(j.target_path||'')}</div>
${j.last_error?`<div class="mt-1 text-xs text-red-400/70 truncate">${esc(j.last_error)}</div>`:''}
<div class="mt-2 w-full bg-slate-800 rounded-full h-1"><div class="h-1 rounded-full ${retryPct>=80?'bg-red-400':retryPct>=50?'bg-yellow-400':'bg-brand'}" style="width:${retryPct}%"></div></div>
${j.next_retry_at?`<div class="mt-1 text-xs text-slate-600">下次重试: ${fmtTime(j.next_retry_at)}</div>`:''}
</div>`}).join(''):'<div class="text-slate-500 text-center py-8">暂无重试任务</div>';
}catch(e){document.getElementById('retryList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmtTime(t){if(!t)return'';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
loadRetries();
</script>
</body></html>
</body></html>
+132 -15
View File
@@ -1,23 +1,140 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 定时调度</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><span class="text-white font-bold text-lg">Nexus</span></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">📤 推送</a><a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">⏰ 调度</a><a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🔄 重试</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><div class="flex items-center justify-between"><h1 class="text-white font-semibold">定时调度</h1><button onclick="showNew()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">+ 新建</button></div></header>
<main class="flex-1 overflow-y-auto p-6"><div id="schedList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
<div id="newForm" class="hidden mt-6 bg-slate-900 rounded-xl border border-slate-800 p-6 space-y-3 max-w-lg">
<input id="sName" placeholder="名称" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm">
<input id="sPath" placeholder="源路径" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm">
<input id="sCron" placeholder="Cron: 0 2 * * *" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm">
<div class="flex gap-2"><button onclick="createSched()" class="px-4 py-2 bg-brand text-white rounded-lg text-sm">保存</button><button onclick="document.getElementById('newForm').classList.add('hidden')" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-sm">取消</button></div>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
<div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div>
<nav class="flex-1 py-3 px-3 space-y-1">
<a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a>
<a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a>
<a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a>
<a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a>
<a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a>
<a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">⏰ 调度</a>
<a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试</a>
<a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a>
<a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a>
</nav>
<div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">定时调度</h1></div>
<button onclick="showAddSched()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">+ 新建</button>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="schedList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
<!-- Add/Edit Schedule Modal -->
<div id="schedModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-lg space-y-3">
<h2 id="schedModalTitle" class="text-white font-semibold text-lg">新建调度</h2>
<input type="hidden" id="editSchedId">
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="sName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="每日推送"></div>
<div><label class="block text-slate-400 text-xs mb-1">源路径</label><input id="sPath" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="/www/wwwroot/project/"></div>
<div><label class="block text-slate-400 text-xs mb-1">Cron 表达式</label><input id="sCron" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm font-mono" placeholder="0 2 * * *"><div class="text-slate-600 text-xs mt-1">分 时 日 月 周 · 例: 0 2 * * * = 每天凌晨2点</div></div>
<div><label class="block text-slate-400 text-xs mb-1">目标服务器</label><select id="sServers" multiple class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm h-32"></select></div>
<div class="flex gap-2 pt-2"><button onclick="saveSched()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideSchedModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
</div>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
async function loadScheds(){const r=await apiFetch(API+'/schedules/');if(!r)return;const s=await r.json();document.getElementById('schedList').innerHTML=s.length?s.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4 flex items-center justify-between"><div><span class="text-white">${esc(s.name)}</span><span class="text-slate-500 text-xs ml-2">${s.cron_expr}</span><span class="text-xs ml-2 ${s.enabled?'text-green-400':'text-red-400'}">${s.enabled?'启用':'禁用'}</span></div><button onclick="deleteSched(${s.id})" class="text-red-400 text-xs hover:underline">删除</button></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无调度</div>'}
function showNew(){document.getElementById('newForm').classList.remove('hidden')}
async function createSched(){await apiFetch(API+'/schedules/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({name:document.getElementById('sName').value,source_path:document.getElementById('sPath').value,cron_expr:document.getElementById('sCron').value,server_ids:'[]',enabled:true})});loadScheds();document.getElementById('newForm').classList.add('hidden')}
async function deleteSched(id){await apiFetch(API+'/schedules/'+id,{method:'DELETE'});loadScheds()}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
loadScheds()
let _schedsCache={};
async function loadScheds(){
try{
const r=await apiFetch(API+'/schedules/');
if(!r)return;
const scheds=await r.json();
_schedsCache={};scheds.forEach(s=>_schedsCache[s.id]=s);
document.getElementById('schedList').innerHTML=scheds.length?scheds.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<button onclick="toggleSched(${s.id},${!s.enabled})" class="w-10 h-6 rounded-full transition-colors ${s.enabled?'bg-brand':'bg-slate-700'} relative shrink-0" title="${s.enabled?'点击禁用':'点击启用'}">
<span class="absolute top-0.5 ${s.enabled?'right-0.5':'left-0.5'} w-5 h-5 bg-white rounded-full shadow transition-all"></span>
</button>
<div>
<span class="text-white font-medium">${esc(s.name)}</span>
<div class="text-slate-500 text-xs mt-0.5"><span class="font-mono">${esc(s.cron_expr)}</span>${s.last_run_at?' · 上次: '+fmtTime(s.last_run_at):''}</div>
</div>
</div>
<div class="flex items-center gap-2">
<button onclick="showEditSched(${s.id})" class="text-slate-400 hover:text-white text-xs transition">编辑</button>
<button onclick="deleteSched(${s.id})" class="text-red-400 text-xs hover:underline">删除</button>
</div>
</div>
</div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无调度</div>';
}catch(e){document.getElementById('schedList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
}
async function loadServersIntoSelect(selectedIds){
const r=await apiFetch(API+'/servers/');
if(!r)return;
const data=await r.json();
const servers=data.items||data;
const sel=document.getElementById('sServers');
sel.innerHTML=servers.map(s=>`<option value="${s.id}" ${selectedIds&&selectedIds.includes(s.id)?'selected':''}>${esc(s.name)} (${esc(s.domain)})</option>`).join('');
}
function showAddSched(){
document.getElementById('editSchedId').value='';
document.getElementById('schedModalTitle').textContent='新建调度';
document.getElementById('sName').value='';
document.getElementById('sPath').value='';
document.getElementById('sCron').value='';
loadServersIntoSelect([]);
document.getElementById('schedModal').classList.remove('hidden');
}
async function showEditSched(id){
const s=_schedsCache[id];if(!s)return;
document.getElementById('editSchedId').value=s.id;
document.getElementById('schedModalTitle').textContent='编辑调度';
document.getElementById('sName').value=s.name||'';
document.getElementById('sPath').value=s.source_path||'';
document.getElementById('sCron').value=s.cron_expr||'';
let serverIds=[];
try{serverIds=JSON.parse(s.server_ids||'[]')}catch(e){}
await loadServersIntoSelect(serverIds);
document.getElementById('schedModal').classList.remove('hidden');
}
function hideSchedModal(){document.getElementById('schedModal').classList.add('hidden')}
async function saveSched(){
const id=document.getElementById('editSchedId').value;
const opts=document.getElementById('sServers').selectedOptions;
const serverIds=Array.from(opts).map(o=>parseInt(o.value));
const body={
name:document.getElementById('sName').value,
source_path:document.getElementById('sPath').value,
cron_expr:document.getElementById('sCron').value,
server_ids:JSON.stringify(serverIds),
enabled:true,
};
if(!body.name||!body.cron_expr){alert('名称和Cron必填');return}
if(id){
await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)});
}else{
await apiFetch(API+'/schedules/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
}
hideSchedModal();loadScheds();
}
async function toggleSched(id,enabled){
await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({enabled})});
loadScheds();
}
async function deleteSched(id){
if(!confirm('确定删除调度?'))return;
await apiFetch(API+'/schedules/'+id,{method:'DELETE'});
loadScheds();
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmtTime(t){if(!t)return'';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
loadScheds();
</script>
</body></html>
</body></html>
+18 -3
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 脚本库</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">📜 脚本库</a><a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🔑 凭据</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a><a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a><a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a><a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">📜 脚本库</a><a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a><a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a><a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试队列</a><a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a><a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><div class="flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">脚本库</h1></div><button onclick="showNewScript()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">+ 新建脚本</button></div></header>
<main class="flex-1 overflow-y-auto p-6">
<div id="scriptList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
@@ -10,15 +10,30 @@
<textarea id="scriptContent" rows="8" placeholder="Shell 命令内容" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm font-mono"></textarea>
<div class="flex gap-2"><button onclick="createScript()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideNewScript()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
</div>
<!-- Execute Modal -->
<div id="execModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md space-y-3">
<h2 class="text-white font-semibold text-lg">执行脚本</h2>
<p id="execScriptName" class="text-slate-400 text-sm"></p>
<div><label class="block text-slate-400 text-xs mb-1">选择服务器</label><select id="execServers" multiple class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm h-32"></select></div>
<div><label class="block text-slate-400 text-xs mb-1">超时(秒)</label><input id="execTimeout" type="number" value="60" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
<div id="execResult" class="hidden p-3 bg-slate-950 rounded-lg text-sm font-mono text-slate-300 max-h-48 overflow-y-auto"></div>
<div class="flex gap-2"><button onclick="doExec()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">执行</button><button onclick="hideExecModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">关闭</button></div>
</div>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
async function loadScripts(){try{const r=await apiFetch(API+'/scripts/');if(!r)return;const scripts=await r.json();document.getElementById('scriptList').innerHTML=scripts.length?scripts.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><div class="flex items-center justify-between"><span class="text-white font-medium">${esc(s.name)}</span><span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400">${s.category||'ops'}</span></div><pre class="mt-2 text-sm text-slate-400 font-mono bg-slate-950 rounded-lg p-3 overflow-x-auto">${esc(s.content?.substring(0,500)||'')}</pre></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无脚本</div>'}catch(e){}
}
let _execScriptId=null,_execScriptCmd='';
async function loadScripts(){try{const r=await apiFetch(API+'/scripts/');if(!r)return;const scripts=await r.json();document.getElementById('scriptList').innerHTML=scripts.length?scripts.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><div class="flex items-center justify-between"><span class="text-white font-medium">${esc(s.name)}</span><div class="flex items-center gap-2"><span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400">${s.category||'ops'}</span><button onclick="showExec(${s.id},'${esc(s.name)}',\`${esc(s.content?.replace(/`/g,'\\`')||'')}\`)" class="text-brand-light text-xs hover:underline">执行</button><button onclick="deleteScript(${s.id})" class="text-red-400 text-xs hover:underline">删除</button></div></div><pre class="mt-2 text-sm text-slate-400 font-mono bg-slate-950 rounded-lg p-3 overflow-x-auto">${esc(s.content?.substring(0,500)||'')}</pre></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无脚本</div>'}catch(e){}}
function showNewScript(){document.getElementById('newScriptForm').classList.remove('hidden')}
function hideNewScript(){document.getElementById('newScriptForm').classList.add('hidden')}
async function createScript(){const b={name:document.getElementById('scriptName').value,category:document.getElementById('scriptCategory').value||'ops',content:document.getElementById('scriptContent').value};await apiFetch(API+'/scripts/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(b)});hideNewScript();loadScripts()}
async function deleteScript(id){if(!confirm('确定删除脚本?'))return;await apiFetch(API+'/scripts/'+id,{method:'DELETE'});loadScripts()}
async function showExec(id,name,cmd){_execScriptId=id;_execScriptCmd=cmd;document.getElementById('execScriptName').textContent=name;document.getElementById('execResult').classList.add('hidden');const r=await apiFetch(API+'/servers/');if(!r)return;const data=await r.json();const servers=data.items||data;document.getElementById('execServers').innerHTML=servers.map(s=>`<option value="${s.id}">${esc(s.name)} (${esc(s.domain)})</option>`).join('');document.getElementById('execModal').classList.remove('hidden')}
function hideExecModal(){document.getElementById('execModal').classList.add('hidden')}
async function doExec(){const opts=document.getElementById('execServers').selectedOptions;const ids=Array.from(opts).map(o=>parseInt(o.value));if(!ids.length){alert('请选择服务器');return}const timeout=parseInt(document.getElementById('execTimeout').value)||60;const el=document.getElementById('execResult');el.classList.remove('hidden');el.textContent='执行中...';const r=await apiFetch(API+'/scripts/exec',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({script_id:_execScriptId,command:_execScriptCmd,server_ids:ids,timeout:timeout})});if(!r){el.textContent='认证失败';return}const d=await r.json();el.textContent=JSON.stringify(d,null,2)}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
loadScripts()
</script>
+40 -2
View File
@@ -18,17 +18,42 @@
<div class="border-t border-slate-800 p-4"><div class="flex items-center justify-between"><span id="sidebarUser" class="text-slate-400 text-sm">...</span><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">服务器列表</h1></div><div class="flex items-center gap-3"><input id="searchInput" type="text" placeholder="搜索服务器..." class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-brand w-48"><a href="/app/terminal.html" id="sshLink" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">SSH终端</a></div></header>
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">服务器列表</h1></div><div class="flex items-center gap-3"><input id="searchInput" type="text" placeholder="搜索服务器..." class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-brand w-48"><a href="/app/terminal.html" id="sshLink" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">SSH终端</a><button onclick="showAddServer()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-white text-sm rounded-lg transition">+ 添加</button></div></header>
<main class="flex-1 overflow-y-auto p-6">
<div id="serversTable" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<table class="w-full text-sm"><thead class="bg-slate-800/50 text-slate-400 text-xs uppercase"><tr><th class="text-left px-4 py-3">状态</th><th class="text-left px-4 py-3">名称</th><th class="text-left px-4 py-3">地址</th><th class="text-left px-4 py-3">平台</th><th class="text-left px-4 py-3">Agent版本</th><th class="text-left px-4 py-3">最后心跳</th><th class="text-right px-4 py-3">操作</th></tr></thead><tbody id="serversTbody" class="divide-y divide-slate-800"><tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">加载中...</td></tr></tbody></table>
</div>
<div class="mt-4 flex items-center justify-between text-xs text-slate-500"><span id="serverCount">共 -- 台</span><div class="flex gap-2"><button onclick="loadServers()" class="px-3 py-1 bg-slate-800 hover:bg-slate-700 rounded-lg transition">刷新</button></div></div>
<!-- Add/Edit Server Modal -->
<div id="serverModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-lg space-y-3">
<h2 id="modalTitle" class="text-white font-semibold text-lg">添加服务器</h2>
<input type="hidden" id="editServerId">
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="srvName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="web-server-01"></div>
<div><label class="block text-slate-400 text-xs mb-1">地址</label><input id="srvDomain" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="192.168.1.10"></div>
</div>
<div class="grid grid-cols-3 gap-3">
<div><label class="block text-slate-400 text-xs mb-1">SSH端口</label><input id="srvPort" type="number" value="22" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
<div><label class="block text-slate-400 text-xs mb-1">用户名</label><input id="srvUser" value="root" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
<div><label class="block text-slate-400 text-xs mb-1">认证方式</label><select id="srvAuth" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option value="key">密钥</option><option value="password">密码</option></select></div>
</div>
<div id="srvPasswordRow"><label class="block text-slate-400 text-xs mb-1">密码</label><input id="srvPassword" type="password" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="SSH密码"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-slate-400 text-xs mb-1">分类</label><input id="srvCategory" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="production"></div>
<div><label class="block text-slate-400 text-xs mb-1">目标路径</label><input id="srvTarget" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="/www/wwwroot/"></div>
</div>
<div><label class="block text-slate-400 text-xs mb-1">备注</label><input id="srvDesc" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="可选"></div>
<div class="flex gap-2 pt-2"><button onclick="saveServer()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideServerModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
</div>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
let selectedServerId = null;
let _serversCache = {};
async function loadServers(){
try{
@@ -37,6 +62,7 @@
const data=await res.json();
const servers=data.items||data;
const total=data.total||servers.length;
_serversCache={};servers.forEach(s=>_serversCache[s.id]=s);
document.getElementById('serverCount').textContent=`${total}`;
const tbody=document.getElementById('serversTbody');
if(!servers.length){tbody.innerHTML='<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">暂无服务器</td></tr>';return}
@@ -47,12 +73,24 @@
<td class="px-4 py-3 text-slate-400">${s.platform_id||s.category||'--'}</td>
<td class="px-4 py-3 text-slate-400">${s.agent_version||'--'}</td>
<td class="px-4 py-3 text-slate-500 text-xs">${fmtTime(s.last_heartbeat)}</td>
<td class="px-4 py-3 text-right"><a href="/app/terminal.html?server_id=${s.id}" class="text-brand-light hover:underline text-xs mr-2">终端</a><button onclick="deleteServer(${s.id})" class="text-red-400 hover:underline text-xs">删除</button></td>
<td class="px-4 py-3 text-right"><a href="/app/terminal.html?server_id=${s.id}" class="text-brand-light hover:underline text-xs mr-2">终端</a><button onclick="event.stopPropagation();showEditServer(${s.id})" class="text-slate-400 hover:underline text-xs mr-2">编辑</button><button onclick="event.stopPropagation();deleteServer(${s.id})" class="text-red-400 hover:underline text-xs">删除</button></td>
</tr>`).join('');
}catch(e){document.getElementById('serversTbody').innerHTML='<tr><td colspan="7" class="px-4 py-8 text-center text-red-400">加载失败: '+esc(e.message)+'</td></tr>'}
}
function selectServer(id){selectedServerId=id;document.querySelectorAll('tbody tr').forEach(r=>r.classList.remove('bg-brand/5'));document.getElementById('row-'+id)?.classList.add('bg-brand/5');document.getElementById('sshLink').href='/app/terminal.html?server_id='+id}
async function deleteServer(id){if(!confirm('确定删除服务器 #'+id+'?'))return;await apiFetch(API+'/servers/'+id,{method:'DELETE'});loadServers()}
function showAddServer(){document.getElementById('editServerId').value='';document.getElementById('modalTitle').textContent='添加服务器';['srvName','srvDomain','srvPassword','srvCategory','srvTarget','srvDesc'].forEach(id=>document.getElementById(id).value='');document.getElementById('srvPort').value='22';document.getElementById('srvUser').value='root';document.getElementById('srvAuth').value='key';document.getElementById('serverModal').classList.remove('hidden')}
function showEditServer(id){const s=_serversCache[id];if(!s)return;document.getElementById('editServerId').value=s.id;document.getElementById('modalTitle').textContent='编辑服务器';document.getElementById('srvName').value=s.name||'';document.getElementById('srvDomain').value=s.domain||'';document.getElementById('srvPort').value=s.port||22;document.getElementById('srvUser').value=s.username||'root';document.getElementById('srvAuth').value=s.auth_method||'key';document.getElementById('srvPassword').value='';document.getElementById('srvCategory').value=s.category||'';document.getElementById('srvTarget').value=s.target_path||'';document.getElementById('srvDesc').value=s.description||'';document.getElementById('serverModal').classList.remove('hidden')}
function hideServerModal(){document.getElementById('serverModal').classList.add('hidden')}
document.getElementById('srvAuth').addEventListener('change',()=>{document.getElementById('srvPasswordRow').style.display=document.getElementById('srvAuth').value==='password'?'':'none'});
async function saveServer(){
const id=document.getElementById('editServerId').value;
const body={name:document.getElementById('srvName').value,domain:document.getElementById('srvDomain').value,port:parseInt(document.getElementById('srvPort').value)||22,username:document.getElementById('srvUser').value||'root',auth_method:document.getElementById('srvAuth').value,category:document.getElementById('srvCategory').value||null,target_path:document.getElementById('srvTarget').value||null,description:document.getElementById('srvDesc').value||null};
if(body.auth_method==='password'){body.password=document.getElementById('srvPassword').value}
if(!body.name||!body.domain){alert('名称和地址必填');return}
if(id){await apiFetch(API+'/servers/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)})}else{await apiFetch(API+'/servers/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)})}
hideServerModal();loadServers()
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function fmtTime(t){if(!t)return'--';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
+79 -12
View File
@@ -1,23 +1,90 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 系统设置</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.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></head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0"><div class="px-5 py-4 border-b border-slate-800"><a href="/app/index.html" class="text-white font-bold text-lg">Nexus</a></div><nav class="flex-1 py-3 px-3 space-y-1"><a href="/app/index.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🏠 仪表盘</a><a href="/app/servers.html" class="block px-3 py-2.5 rounded-lg text-slate-400 hover:text-white text-sm">🖥 服务器</a><a href="/app/settings.html" class="block px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium">⚙️ 设置</a></nav><div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs">退出</button></div></aside>
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><h1 class="text-white font-semibold">系统设置</h1></header>
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0">
<div class="px-5 py-4 border-b border-slate-800"><div class="flex items-center gap-3"><div class="w-8 h-8 rounded-lg bg-brand/20 flex items-center justify-center"><svg class="w-5 h-5 text-brand-light" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg></div><span class="text-white font-bold text-lg">Nexus</span></div></div>
<nav class="flex-1 py-3 px-3 space-y-1">
<a href="/app/index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🏠 仪表盘</a>
<a href="/app/servers.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🖥 服务器</a>
<a href="/app/push.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📤 推送</a>
<a href="/app/scripts.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📜 脚本库</a>
<a href="/app/credentials.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔑 凭据</a>
<a href="/app/schedules.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">⏰ 调度</a>
<a href="/app/retries.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">🔄 重试队列</a>
<a href="/app/audit.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 text-sm transition">📋 审计</a>
<a href="/app/settings.html" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-brand/10 text-brand-light text-sm font-medium transition">⚙️ 设置</a>
</nav>
<div class="border-t border-slate-800 p-4"><button onclick="doLogout()" class="text-slate-500 hover:text-red-400 text-xs transition">退出</button></div>
</aside>
<div class="flex-1 flex flex-col min-w-0">
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">系统设置</h1></div>
</header>
<main class="flex-1 overflow-y-auto p-6 max-w-2xl mx-auto w-full">
<div id="settingsForm" class="bg-slate-900 rounded-xl border border-slate-800 p-6 space-y-4">
<div class="text-slate-400 text-sm mb-4">可修改的运行时配置SECRET_KEY/API_KEY/DATABASE_URL 不可在此修改</div>
<div id="settingsList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
<div class="mb-6 bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="text-slate-400 text-xs">以下为可修改的运行时配置SECRET_KEY / API_KEY / DATABASE_URL 为安全锁定项,不可通过此页面修改</div>
</div>
<!-- Brand Settings -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 mb-4">
<h2 class="text-white font-medium mb-4">品牌</h2>
<div id="brandSection" class="space-y-3"><div class="text-slate-500 text-center py-4 text-sm">加载中...</div></div>
</div>
<!-- Alert Thresholds -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 mb-4">
<h2 class="text-white font-medium mb-4">告警阈值 (%)</h2>
<div id="alertSection" class="space-y-3"><div class="text-slate-500 text-center py-4 text-sm">加载中...</div></div>
</div>
<!-- Infrastructure -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 mb-4">
<h2 class="text-white font-medium mb-4">基础设施</h2>
<div id="infraSection" class="space-y-3"><div class="text-slate-500 text-center py-4 text-sm">加载中...</div></div>
</div>
<!-- Telegram -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6">
<h2 class="text-white font-medium mb-4">Telegram 通知</h2>
<div id="telegramSection" class="space-y-3"><div class="text-slate-500 text-center py-4 text-sm">加载中...</div></div>
</div>
<div id="saveMsg" class="hidden mt-4 text-center text-green-400 text-sm">已保存</div>
</main>
</div>
<script src="/app/api.js"></script>
<script>
const SECTIONS={
brand:{keys:['system_name','system_title'],labels:{system_name:'系统名称',system_title:'页面标题'}},
alert:{keys:['cpu_alert_threshold','mem_alert_threshold','disk_alert_threshold'],labels:{cpu_alert_threshold:'CPU 告警阈值',mem_alert_threshold:'内存告警阈值',disk_alert_threshold:'磁盘告警阈值'}},
infra:{keys:['redis_url','pool_size','max_overflow','agent_heartbeat_interval'],labels:{redis_url:'Redis URL',pool_size:'连接池大小',max_overflow:'最大溢出',agent_heartbeat_interval:'心跳间隔(秒)'}},
telegram:{keys:['telegram_bot_token','telegram_chat_id'],labels:{telegram_bot_token:'Bot Token',telegram_chat_id:'Chat ID'}},
};
let _allSettings={};
async function loadSettings(){
try{const r=await apiFetch(API+'/settings/');if(!r)return;const settings=await r.json();
const mutableKeys=['system_name','system_title','redis_url','telegram_bot_token','telegram_chat_id','cpu_alert_threshold','mem_alert_threshold','disk_alert_threshold','agent_heartbeat_interval','pool_size','max_overflow'];
document.getElementById('settingsList').innerHTML=settings.filter(s=>mutableKeys.includes(s.key)).map(s=>`<div class="flex items-center gap-3"><label class="text-slate-400 text-sm w-40 shrink-0">${s.key}</label><input id="set_${s.key}" value="${esc(s.value||'')}" class="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><button onclick="saveSetting('${s.key}')" class="px-3 py-2 bg-brand hover:bg-brand-dark text-white text-xs rounded-lg shrink-0">保存</button></div>`).join('')}catch(e){}
try{
const r=await apiFetch(API+'/settings/');if(!r)return;
const settings=await r.json();
_allSettings={};settings.forEach(s=>_allSettings[s.key]=s.value);
for(const[section,cfg]of Object.entries(SECTIONS)){
const el=document.getElementById(section+'Section');
el.innerHTML=cfg.keys.map(key=>`<div class="flex items-center gap-3">
<label class="text-slate-400 text-sm w-36 shrink-0">${cfg.labels[key]||key}</label>
<input id="set_${key}" value="${esc(_allSettings[key]||'')}" class="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
</div>`).join('');
}
}catch(e){}
}
async function saveSetting(key){const v=document.getElementById('set_'+key).value;await apiFetch(API+'/settings/'+key,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({value:v})})}
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
loadSettings()
async function saveSetting(key){
const v=document.getElementById('set_'+key).value;
await apiFetch(API+'/settings/'+key,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({value:v})});
const msg=document.getElementById('saveMsg');
msg.classList.remove('hidden');
setTimeout(()=>msg.classList.add('hidden'),2000);
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
loadSettings();
</script>
</body></html>
</body></html>
+9 -4
View File
@@ -22,7 +22,7 @@ if (file_exists($lockFile)) {
<h2 style="color:#dc2626;">⚠ 安装已锁定</h2>
<p>系统已完成安装,install.php 已重命名为 install.php.locked。</p>
<p>如需重新安装,请删除 <code>install.php.locked</code> 文件。</p>
<a href="login.php" style="color:#3b82f6;">前往登录 →</a>
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
</div>');
}
@@ -31,7 +31,7 @@ if (file_exists($envFile)) {
<h2 style="color:#f59e0b;">⚠ .env 已存在</h2>
<p>检测到 .env 配置文件,系统可能已安装。</p>
<p>如需重新安装,请先删除 <code>.env</code> 和 <code>web/data/config.php</code>。</p>
<a href="login.php" style="color:#3b82f6;">前往登录 →</a>
<a href="/app/login.html" style="color:#3b82f6;">前往登录 →</a>
</div>');
}
@@ -1181,7 +1181,12 @@ stdout_logfile_maxbytes=50MB</pre>
<div class="post-item-desc">宝塔 → 网站 → 伪静态 → 粘贴:</div>
<div class="code-wrap">
<button class="copy-btn" onclick="copyBlock('nginxRewrite')">复制</button>
<pre id="nginxRewrite">try_files $uri $uri/ /index.php?$query_string;</pre>
<pre id="nginxRewrite">location /app/ {
try_files $uri $uri/ /app/index.html;
}
location / {
try_files $uri $uri/ =404;
}</pre>
</div>
</div>
@@ -1229,7 +1234,7 @@ stdout_logfile_maxbytes=50MB</pre>
</div>
<div class="btn-group" style="justify-content:center; margin-top:24px;">
<a href="index.php" class="btn btn-success" style="font-size:16px; padding:14px 32px;">进入管理面板 →</a>
<a href="/app/index.html" class="btn btn-success" style="font-size:16px; padding:14px 32px;">进入管理面板 →</a>
</div>
</div>