59 lines
8.4 KiB
HTML
59 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>Overview - ToNav</title><link rel="stylesheet" href="/static/css/admin.css">
|
||
<script>document.documentElement.classList.toggle('dark',localStorage.getItem('tonav-theme')==='dark');</script>
|
||
</head>
|
||
<body>
|
||
<div class="admin-shell">
|
||
<aside class="admin-sidebar">
|
||
<div class="admin-brand">ToNav</div>
|
||
<div class="admin-label">OVERVIEW</div>
|
||
<a class="admin-nav active" href="/admin/dashboard">Overview</a>
|
||
<div class="admin-label" style="margin-top:22px">SERVICES</div>
|
||
<a class="admin-nav" href="/admin/services">Services</a>
|
||
<a class="admin-nav" href="/admin/categories">Categories</a>
|
||
<div class="admin-label" style="margin-top:22px">SYSTEM</div>
|
||
<a class="admin-nav" href="#settings" onclick="showSettings();return false">Settings</a>
|
||
<div class="admin-sidebar-foot"><a href="/">← Open navigation</a><br><a href="/admin/logout">Sign out</a></div>
|
||
</aside>
|
||
<main class="admin-content"><div class="admin-main">
|
||
<header class="admin-topbar"><div><h1>Overview</h1><div class="admin-sub">Service directory and system status</div></div><div class="top-actions"><button class="btn" onclick="toggleTheme()">Theme</button><a class="btn" href="/">Open site</a></div></header>
|
||
<section class="section"><div class="section-title">System status</div>
|
||
<div class="metric-strip"><div class="metric"><div class="metric-value">{{ .service_count }}</div><div class="metric-label">Services</div></div><div class="metric"><div class="metric-value">{{ .category_count }}</div><div class="metric-label">Categories</div></div><div class="metric"><div class="metric-value green">{{ .online_count }}</div><div class="metric-label">Online</div></div><div class="metric"><div class="metric-value red">{{ .offline_count }}</div><div class="metric-label">Offline</div></div></div></section>
|
||
<section class="section"><div class="section-title">Quick actions</div><div class="panel action-list">
|
||
<a class="action" href="/admin/services"><strong>Services</strong><span>Manage service entries</span></a>
|
||
<a class="action" href="/admin/categories"><strong>Categories</strong><span>Organize your directory</span></a>
|
||
<a class="action" href="#settings" onclick="showSettings();return false"><strong>Settings</strong><span>Site, backup and MCP</span></a>
|
||
<div class="action" onclick="runBackup()"><strong>Backup</strong><span>Run cloud backup now</span></div>
|
||
</div></section>
|
||
</div></main></div>
|
||
<div id="settingsModal" class="modal-overlay"><div class="modal-content">
|
||
<div class="modal-head"><h2>System settings</h2><button class="close" onclick="closeSettings()">×</button></div>
|
||
<div class="form-group"><label>Language</label><select id="cfg_language" class="input"><option value="en">English</option><option value="zh-CN">Chinese (Simplified)</option></select></div><div class="form-group"><label>Site title</label><input id="cfg_site_title" class="input"></div>
|
||
<div class="form-group"><label>WebDAV URL</label><input id="cfg_url" class="input"></div>
|
||
<div class="form-row"><div class="form-group"><label>Username</label><input id="cfg_user" class="input"></div><div class="form-group"><label>Password</label><input id="cfg_pass" type="password" class="input"></div></div>
|
||
<div class="form-group"><label><input type="checkbox" id="cfg_auto_backup"> Daily backup at 03:00</label></div>
|
||
<div class="section-title">MCP Server</div><div class="notice"><label><input type="checkbox" id="cfg_mcp_enabled" onchange="toggleMCP()"> Enable MCP endpoint</label><div id="mcpTokenRow" style="display:none;margin-top:12px"><label>Token</label><div class="form-row"><input id="cfg_mcp_token" class="input" readonly><button class="btn" onclick="copyMCPToken(this)">Copy</button><button class="btn" onclick="regenMCPToken()">Regenerate</button></div><div style="margin-top:6px">POST/GET /mcp · changes apply immediately</div></div></div>
|
||
<div class="section-title" style="margin-top:20px">Cloud backups</div><div class="panel" style="padding:12px"><div id="backupList" class="admin-sub">Loading...</div></div>
|
||
<div class="modal-actions"><button class="btn" onclick="closeSettings()">Close</button><button class="btn btn-primary" onclick="saveSettings()">Save changes</button></div>
|
||
</div></div>
|
||
<script>
|
||
function toggleTheme(){const dark=!document.documentElement.classList.contains('dark');document.documentElement.classList.toggle('dark',dark);localStorage.setItem('tonav-theme',dark?'dark':'light')}
|
||
function closeSettings(){document.getElementById('settingsModal').style.display='none'}
|
||
async function showSettings(){const r=await fetch('/admin/api/settings'),d=await r.json();cfg_language.value=d.language||'en';cfg_site_title.value=d.site_title||'';cfg_url.value=d.webdav_url||'';cfg_user.value=d.webdav_user||'';cfg_pass.value=d.webdav_password||'';cfg_auto_backup.checked=d.auto_backup==='true';document.getElementById('settingsModal').style.display='flex';loadMCPStatus();loadCloudBackups()}
|
||
async function loadMCPStatus(){try{const r=await fetch('/admin/api/mcp/status'),d=await r.json();cfg_mcp_enabled.checked=d.enabled;mcpTokenRow.style.display=d.enabled?'block':'none';cfg_mcp_token.value=d.token_masked||'Not generated'}catch(e){}}
|
||
async function toggleMCP(){const enabled=cfg_mcp_enabled.checked;try{const r=await fetch('/admin/api/mcp/enabled',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled})}),d=await r.json();if(!d.success)throw Error(d.message)}catch(e){alert(e.message||t('Failed'));cfg_mcp_enabled.checked=!enabled;mcpTokenRow.style.display=enabled?'none':'block'}}
|
||
async function copyMCPToken(btn){try{const r=await fetch('/admin/api/mcp/token'),d=await r.json();if(!d.success)throw Error(d.message);await navigator.clipboard.writeText(d.token);const old=btn.textContent;btn.textContent='Copied';setTimeout(()=>btn.textContent=old,1200)}catch(e){alert(t('Copy failed: ') + e.message)}}
|
||
async function regenMCPToken(){if(!confirm(t('Regenerate token? The old token will stop working.')))return;const r=await fetch('/admin/api/mcp/token',{method:'POST'}),d=await r.json();if(d.success){alert(t('New token:')+'\n\n'+d.token);loadMCPStatus()}else alert(d.message)}
|
||
async function loadCloudBackups(){const el=backupList;try{const r=await fetch('/admin/api/backup/list'),d=await r.json();if(!d.files?.length){el.textContent='No cloud backups';return}el.innerHTML=d.files.map(f=>`<div style="padding:8px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:10px"><span>${f.name}<small style="display:block;color:var(--faint)">${formatTime(f.mod_time)} · ${formatSize(f.size)}</small></span><span><button class="btn" onclick="restoreBackup('${encodeURIComponent(f.name)}')">Restore</button> <button class="btn btn-danger" onclick="deleteBackup('${encodeURIComponent(f.name)}')">Delete</button></span></div>`).join('')}catch(e){el.textContent='Unable to load backups'}}
|
||
function formatSize(b){if(!b)return'';if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';return(b/1048576).toFixed(1)+' MB'}
|
||
function formatTime(s){if(!s)return'';const d=new Date(s);return isNaN(d)?s:d.toLocaleString('zh-CN')}
|
||
async function saveSettings(){const data={language:cfg_language.value,site_title:cfg_site_title.value,webdav_url:cfg_url.value,webdav_user:cfg_user.value,webdav_password:cfg_pass.value,auto_backup:cfg_auto_backup.checked?'true':'false'};const r=await fetch('/admin/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}),d=await r.json();alert(d.message||t('Saved'));if(d.success){location.reload()}}
|
||
async function runBackup(){if(!confirm(t('Run cloud backup now?')))return;const r=await fetch('/admin/api/backup/webdav',{method:'POST'}),d=await r.json();alert(d.message||t('Backup complete'))}
|
||
async function restoreBackup(name){name=decodeURIComponent(name);if(!confirm(t('Restore ')+name+t('? Current database will be backed up first.')))return;const r=await fetch('/admin/api/backup/restore?name='+encodeURIComponent(name),{method:'POST'}),d=await r.json();alert(d.message||t('Done'));if(d.success)location.reload()}
|
||
async function deleteBackup(name){name=decodeURIComponent(name);if(!confirm(t('Delete ')+name+t('?')))return;const r=await fetch('/admin/api/backup/delete?name='+encodeURIComponent(name),{method:'DELETE'}),d=await r.json();if(d.success)loadCloudBackups();else alert(d.message)}
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeSettings()});
|
||
</script><script src="/static/js/i18n.js"></script></body></html>
|