Files
ToNav-go/templates/admin/dashboard.html
T

59 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<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>