Add system settings, one-click deploy, and simplify embed UI.

Remove Gate scout auto-seeding and the manual hub login button while keeping Gate login and instance SSO. Add password change and database backup/restore, plus deploy scripts with env auto-generation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-12 11:44:12 +08:00
parent f7ce6f1058
commit 3149770887
15 changed files with 635 additions and 242 deletions
+1
View File
@@ -6,6 +6,7 @@
<nav>
<a href="{{ url_for('index') }}">返回导航</a>
<a href="{{ url_for('admin_services') }}">服务管理</a>
<a href="{{ url_for('admin_settings') }}">系统设置</a>
<a href="{{ url_for('logout') }}">退出</a>
</nav>
</header>
+1
View File
@@ -6,6 +6,7 @@
<nav>
<a href="{{ url_for('index') }}">返回导航</a>
<a href="{{ url_for('admin_groups') }}">分组管理</a>
<a href="{{ url_for('admin_settings') }}">系统设置</a>
<a href="{{ url_for('logout') }}">退出</a>
</nav>
</header>
+90
View File
@@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}系统设置 · 本地导航{% endblock %}
{% block body %}
<header class="topbar">
<h1>系统设置</h1>
<nav>
<a href="{{ url_for('index') }}">返回导航</a>
<a href="{{ url_for('admin_groups') }}">分组管理</a>
<a href="{{ url_for('admin_services') }}">服务管理</a>
<a href="{{ url_for('logout') }}">退出</a>
</nav>
</header>
<div class="page-wrap">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-wrap">
{% for cat, msg in messages %}
<div class="flash {{ cat }}">{{ msg }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<section class="settings-section">
<h2>账户安全</h2>
<p class="hint">当前用户:<strong>{{ current_user.username }}</strong></p>
<form method="post" novalidate>
{{ pwd_form.hidden_tag() }}
<div class="form-row">
{{ pwd_form.current_password.label }}
{{ pwd_form.current_password() }}
{% if pwd_form.current_password.errors %}
<div class="errors">{{ pwd_form.current_password.errors[0] }}</div>
{% endif %}
</div>
<div class="form-row">
{{ pwd_form.new_password.label }}
{{ pwd_form.new_password() }}
{% if pwd_form.new_password.errors %}
<div class="errors">{{ pwd_form.new_password.errors[0] }}</div>
{% endif %}
</div>
<div class="form-row">
{{ pwd_form.confirm_password.label }}
{{ pwd_form.confirm_password() }}
{% if pwd_form.confirm_password.errors %}
<div class="errors">{{ pwd_form.confirm_password.errors[0] }}</div>
{% endif %}
</div>
<div class="toolbar" style="margin-top: 1rem">
{{ pwd_form.submit(class="btn btn-primary", style="width: auto") }}
</div>
</form>
</section>
<section class="settings-section">
<h2>数据备份与恢复</h2>
<p class="hint">
数据库文件:<code>{{ db_path }}</code>
{% if db_size %}
(约 {{ "%.1f"|format(db_size / 1024) }} KB
{% else %}
(文件不存在或为空)
{% endif %}
</p>
<div class="toolbar">
<a class="btn btn-secondary" href="{{ url_for('admin_settings_backup') }}" style="width: auto">一键备份</a>
</div>
<form
method="post"
enctype="multipart/form-data"
novalidate
onsubmit="return confirm('恢复将覆盖当前数据库,是否继续?恢复前会自动生成 .bak 备份。');"
>
{{ restore_form.hidden_tag() }}
<div class="form-row">
{{ restore_form.backup_file.label }}
{{ restore_form.backup_file() }}
{% if restore_form.backup_file.errors %}
<div class="errors">{{ restore_form.backup_file.errors[0] }}</div>
{% endif %}
</div>
<p class="hint">恢复完成后请重启 LocalNav 服务。</p>
<div class="toolbar" style="margin-top: 1rem">
{{ restore_form.submit(class="btn btn-secondary", style="width: auto") }}
</div>
</form>
</section>
</div>
{% endblock %}
+4 -36
View File
@@ -9,6 +9,7 @@
<span class="user">{{ current_user.username }}</span>
<a href="{{ url_for('admin_groups') }}">分组管理</a>
<a href="{{ url_for('admin_services') }}">服务管理</a>
<a href="{{ url_for('admin_settings') }}">系统设置</a>
<a href="{{ url_for('logout') }}">退出</a>
</nav>
</header>
@@ -131,20 +132,11 @@
type="button"
class="btn btn-secondary btn-toolbar-refresh"
id="frame-gate-login"
title="通过本地导航代登录 Gate 扫单(需配置 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD"
title="通过本地导航代登录 Gate 服务(需配置 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD"
hidden
>
Gate 登录
</button>
<button
type="button"
class="btn btn-secondary btn-toolbar-refresh"
id="frame-hub-login"
title="通过本地导航代登录云端中控(需配置 NAV_HUB_USERNAME / NAV_HUB_PASSWORD"
hidden
>
中控登录
</button>
<button
type="button"
class="btn btn-secondary btn-toolbar-refresh"
@@ -228,7 +220,6 @@
var btnOpenTab = document.getElementById("frame-open-tab");
var btnBack = document.getElementById("frame-back-overview");
var btnBackHub = document.getElementById("frame-back-hub");
var btnHubLogin = document.getElementById("frame-hub-login");
var btnGateLogin = document.getElementById("frame-gate-login");
var btnInstanceSso = document.getElementById("frame-instance-sso");
var currentBaseUrl = "";
@@ -282,10 +273,6 @@
if (btnInstanceSso) btnInstanceSso.hidden = !show;
}
function toggleHubLoginBtn(show) {
if (btnHubLogin) btnHubLogin.hidden = !show;
}
function toggleGateLoginBtn(show) {
if (btnGateLogin) btnGateLogin.hidden = !show;
}
@@ -324,12 +311,10 @@
currentViewMode = "hub";
toggleInstanceBackBtn(false);
toggleInstanceSsoBtn(false);
toggleHubLoginBtn(true);
return;
}
currentViewMode = "hub-instance";
toggleInstanceBackBtn(true);
toggleHubLoginBtn(false);
toggleInstanceSsoBtn(!!(instanceNavCtx && instanceNavCtx.exchangeId));
}
@@ -415,7 +400,6 @@
};
currentViewMode = "hub-instance";
if (data.title) nameEl.textContent = data.title;
toggleHubLoginBtn(false);
toggleInstanceBackBtn(true);
toggleInstanceSsoBtn(!!instanceNavCtx.exchangeId);
return;
@@ -455,7 +439,6 @@
};
currentViewMode = "hub-instance";
nameEl.textContent = instanceNavCtx.title;
toggleHubLoginBtn(false);
toggleInstanceBackBtn(true);
applyIframeUrl(data.url);
});
@@ -508,7 +491,6 @@
currentNextPath = st.nextPath || currentNextPath;
nameEl.textContent = st.name || nameEl.textContent;
}
toggleHubLoginBtn(isHubEmbed(currentEmbedKind));
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
hubLoginViaProxy(function (ok) {
if (!ok) applyIframeUrl(currentOpenUrl || currentBaseUrl);
@@ -553,19 +535,18 @@
dashboard.hidden = true;
frameStack.hidden = false;
frame.hidden = false;
toggleHubLoginBtn(isHubEmbed(currentEmbedKind) && !hubAutoLogin);
toggleGateLoginBtn(isGateScoutEmbed(currentEmbedKind) && !gateScoutAutoLogin);
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
hubLoginViaProxy(function (ok, err) {
if (!ok) {
applyIframeUrl(url);
toggleHubLoginBtn(true);
if (err) console.warn("[LocalNav] 中控代登录失败:", err);
}
});
var nav = preferredNav || findNavLink(url);
setActive(nav);
return;
}
toggleGateLoginBtn(isGateScoutEmbed(currentEmbedKind) && !gateScoutAutoLogin);
if (isGateScoutEmbed(currentEmbedKind) && gateScoutAutoLogin) {
gateScoutLoginViaProxy(function (ok, err) {
if (!ok) {
@@ -682,7 +663,6 @@
frame.hidden = true;
frameStack.hidden = true;
dashboard.hidden = false;
toggleHubLoginBtn(false);
toggleGateLoginBtn(false);
toggleInstanceSsoBtn(false);
setActive(null);
@@ -731,18 +711,6 @@
});
}
if (btnHubLogin) {
btnHubLogin.addEventListener("click", function () {
btnHubLogin.disabled = true;
hubLoginViaProxy(function (ok, err) {
btnHubLogin.disabled = false;
if (!ok && err) {
window.alert("中控登录失败:\n" + err + "\n\n请检查 LocalNav .env 的 NAV_HUB_USERNAME / NAV_HUB_PASSWORD 是否与云端 hub .env 一致。");
}
});
});
}
if (btnInstanceSso) {
btnInstanceSso.addEventListener("click", function () {
btnInstanceSso.disabled = true;