feat: add web admin panel for node management

Add Flask panel with login, add/delete nodes, and share link copy.
Generate sing-box config from SQLite; add uninstall script and clean install flow.
Panel served at https://DOMAIN:8444 via nginx.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 09:10:19 +08:00
parent e8631a0e10
commit bccf6cfdce
21 changed files with 1069 additions and 305 deletions
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}jiedian 面板{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
{% block body %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
{% extends "base.html" %}
{% block title %}节点管理 · jiedian{% endblock %}
{% block body %}
<header class="topbar">
<div>
<strong>jiedian 面板</strong>
<span class="muted"> · {{ domain }}</span>
</div>
<a class="btn ghost" href="{{ url_for('logout') }}">退出</a>
</header>
<main class="container">
<section class="hero">
<div>
<h1>节点列表</h1>
<p class="muted">VPS {{ vps_ip }} · Reality 443 · Hysteria2 8443</p>
</div>
<button id="addBtn" class="btn primary">+ 添加节点</button>
</section>
<div id="toast" class="toast hidden"></div>
<div id="nodeList" class="node-list">
{% for node in nodes %}
<article class="node-card" data-id="{{ node.id }}">
<div class="node-head">
<h2>{{ node.name }}</h2>
<span class="tag">{{ node.created_at[:10] }}</span>
</div>
<div class="field">
<label>VLESS + Reality</label>
<div class="copy-row">
<input readonly value="{{ node.links.vless }}">
<button class="btn" data-copy="{{ node.links.vless }}">复制</button>
</div>
</div>
<div class="field">
<label>Hysteria2</label>
<div class="copy-row">
<input readonly value="{{ node.links.hy2 }}">
<button class="btn" data-copy="{{ node.links.hy2 }}">复制</button>
</div>
</div>
<div class="node-actions">
<button class="btn danger delete-btn" data-id="{{ node.id }}">删除</button>
</div>
</article>
{% endfor %}
</div>
</main>
<div id="modal" class="modal hidden">
<div class="modal-card">
<h3>添加节点</h3>
<label>节点名称</label>
<input id="nodeName" type="text" placeholder="例如:手机、电脑">
<div class="modal-actions">
<button id="cancelBtn" class="btn ghost">取消</button>
<button id="confirmAddBtn" class="btn primary">创建</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='app.js') }}"></script>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}登录 · jiedian{% endblock %}
{% block body %}
<div class="auth-wrap">
<div class="auth-card">
<h1>jiedian 管理面板</h1>
<p class="muted">登录后管理节点与分享链接</p>
{% if error %}
<div class="alert">{{ error }}</div>
{% endif %}
<form method="post" class="form">
<label>用户名</label>
<input type="text" name="username" autocomplete="username" required autofocus>
<label>密码</label>
<input type="password" name="password" autocomplete="current-password" required>
<button type="submit" class="btn primary">登录</button>
</form>
</div>
</div>
{% endblock %}