feat:自动更换ip+流量监控
This commit is contained in:
430
templates/machines.html
Normal file
430
templates/machines.html
Normal file
@@ -0,0 +1,430 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}节点管理 - ProxyAuto Pro{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>节点管理</h1>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="collapsible" id="add-machine-section">
|
||||
<button class="collapsible-header" onclick="toggleCollapsible('add-machine-section')">
|
||||
<svg class="collapsible-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<span>添加新节点</span>
|
||||
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="collapsible-content" {% if machines|length > 0 %}style="display: none;"{% endif %}>
|
||||
<form id="add-machine-form" class="form-card inline-form">
|
||||
<h3 class="form-section-title">基本信息</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="new_name">节点名称</label>
|
||||
<input type="text" id="new_name" name="name" placeholder="例如: US-East-Proxy" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_aws_service">AWS 服务</label>
|
||||
<select id="new_aws_service" name="aws_service">
|
||||
<option value="ec2">EC2</option>
|
||||
<option value="lightsail">Lightsail</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="new_aws_region">AWS 区域 (Region)</label>
|
||||
<input type="text" id="new_aws_region" name="aws_region" value="us-east-1" placeholder="例如: us-west-2" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_instance_id" id="new_instance_id_label">EC2 Instance ID</label>
|
||||
<input type="text" id="new_instance_id" name="instance_id" placeholder="例如: i-0123456789abcdef0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">域名绑定 (可选)</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="new_cf_zone_id">Cloudflare Zone ID</label>
|
||||
<input type="text" id="new_cf_zone_id" name="cf_zone_id" placeholder="留空则不绑定域名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_cf_record_name">DNS 记录名称</label>
|
||||
<input type="text" id="new_cf_record_name" name="cf_record_name" placeholder="例如: api.example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="new_cf_proxied" name="cf_proxied">
|
||||
<span class="checkbox-text">启用 Cloudflare 代理 (橙色云朵)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">自动更换设置</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="new_change_interval">更换间隔 (分钟)</label>
|
||||
<input type="number" id="new_change_interval" name="change_interval_minutes" value="60" min="1">
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="new_auto_enabled" name="auto_enabled">
|
||||
<span class="checkbox-text">启用自动更换</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">流量预警 (可选)</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="new_traffic_alert_enabled" name="traffic_alert_enabled" onchange="toggleTrafficAlertFields('new')">
|
||||
<span class="checkbox-text">启用流量预警</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="new_traffic_limit_group" style="display: none;">
|
||||
<label for="new_traffic_alert_limit_gb" id="new_traffic_limit_label">流量限制 (GB)</label>
|
||||
<input type="number" id="new_traffic_alert_limit_gb" name="traffic_alert_limit_gb" step="0.1" min="0" placeholder="例如: 100">
|
||||
<small class="form-hint" id="new_traffic_limit_hint">EC2: 上传流量预警 / Lightsail: 总流量预警</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<div class="form-group">
|
||||
<label for="new_note">备注 (可选)</label>
|
||||
<input type="text" id="new_note" name="note" placeholder="例如: 生产环境主节点">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="new_enabled" name="enabled" checked>
|
||||
<span class="checkbox-text">立即启用</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">添加节点</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<section class="section">
|
||||
{% if not machines %}
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>
|
||||
<h3>暂无节点</h3>
|
||||
<p>请点击上方"添加新节点"开始配置</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="machine-list">
|
||||
{% for machine in machines %}
|
||||
<div class="machine-card" data-id="{{ machine.id }}">
|
||||
<div class="machine-header">
|
||||
<div class="machine-info">
|
||||
<span class="status-dot {% if machine.enabled %}active{% else %}inactive{% endif %}"></span>
|
||||
<span class="machine-name">{{ machine.name }}</span>
|
||||
{% if machine.auto_enabled %}
|
||||
<span class="badge badge-auto">自动</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="machine-meta">
|
||||
<span class="meta-item">{{ machine.aws_service|upper }}</span>
|
||||
<span class="meta-item">{{ machine.aws_region }}</span>
|
||||
{% if machine.current_ip %}
|
||||
<span class="meta-item ip-badge">{{ machine.current_ip }}</span>
|
||||
{% endif %}
|
||||
{% if machine.cf_record_name %}
|
||||
<span class="meta-item domain-badge">{{ machine.cf_record_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="machine-actions">
|
||||
<button class="btn btn-sm btn-ghost" onclick="toggleEdit({{ machine.id }})">编辑</button>
|
||||
<button class="btn btn-sm btn-danger-ghost" onclick="deleteMachine({{ machine.id }}, '{{ machine.name }}')">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if machine.note %}
|
||||
<div class="machine-note-row">{{ machine.note }}</div>
|
||||
{% endif %}
|
||||
<div class="machine-edit" id="edit-{{ machine.id }}" style="display: none;">
|
||||
<form class="edit-form" onsubmit="updateMachine(event, {{ machine.id }})">
|
||||
<h3 class="form-section-title">基本信息</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>名称</label>
|
||||
<input type="text" name="name" value="{{ machine.name }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>服务类型</label>
|
||||
<select name="aws_service">
|
||||
<option value="ec2" {% if machine.aws_service == 'ec2' %}selected{% endif %}>EC2</option>
|
||||
<option value="lightsail" {% if machine.aws_service == 'lightsail' %}selected{% endif %}>Lightsail</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>区域</label>
|
||||
<input type="text" name="aws_region" value="{{ machine.aws_region }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>实例 ID</label>
|
||||
<input type="text" name="instance_id" value="{{ machine.aws_instance_id }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">域名绑定</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Cloudflare Zone ID</label>
|
||||
<input type="text" name="cf_zone_id" value="{{ machine.cf_zone_id or '' }}" placeholder="留空则不绑定域名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>DNS 记录名称</label>
|
||||
<input type="text" name="cf_record_name" value="{{ machine.cf_record_name or '' }}" placeholder="例如: api.example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="cf_proxied" {% if machine.cf_proxied %}checked{% endif %}>
|
||||
<span class="checkbox-text">启用 Cloudflare 代理</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">自动更换设置</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>更换间隔 (分钟)</label>
|
||||
<input type="number" name="change_interval_minutes" value="{{ machine.change_interval_seconds // 60 }}" min="1">
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="auto_enabled" {% if machine.auto_enabled %}checked{% endif %}>
|
||||
<span class="checkbox-text">启用自动更换</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<h3 class="form-section-title">流量预警</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="traffic_alert_enabled" {% if machine.traffic_alert_enabled %}checked{% endif %} onchange="toggleTrafficAlertFields('edit-{{ machine.id }}')">
|
||||
<span class="checkbox-text">启用流量预警</span>
|
||||
</label>
|
||||
{% if machine.traffic_alert_triggered %}
|
||||
<span class="badge" style="background: var(--error-soft); color: var(--error); margin-left: 0.5rem;">已触发预警</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group traffic-limit-group" {% if not machine.traffic_alert_enabled %}style="display: none;"{% endif %}>
|
||||
<label>流量限制 (GB)</label>
|
||||
<input type="number" name="traffic_alert_limit_gb" value="{{ machine.traffic_alert_limit_gb or '' }}" step="0.1" min="0" placeholder="例如: 100">
|
||||
<small class="form-hint">{{ 'Lightsail: 总流量预警' if machine.aws_service == 'lightsail' else 'EC2: 上传流量预警' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if machine.traffic_alert_triggered %}
|
||||
<div class="form-group" style="margin-top: 0.5rem;">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="resetTrafficAlert({{ machine.id }})">重置预警状态</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<div class="form-group">
|
||||
<label>备注</label>
|
||||
<input type="text" name="note" value="{{ machine.note or '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="enabled" {% if machine.enabled %}checked{% endif %}>
|
||||
<span class="checkbox-text">启用</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions inline">
|
||||
<button type="submit" class="btn btn-primary btn-sm">保存修改</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick="toggleEdit({{ machine.id }})">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 切换服务类型标签
|
||||
document.getElementById('new_aws_service').addEventListener('change', function() {
|
||||
const label = document.getElementById('new_instance_id_label');
|
||||
const input = document.getElementById('new_instance_id');
|
||||
const trafficHint = document.getElementById('new_traffic_limit_hint');
|
||||
if (this.value === 'lightsail') {
|
||||
label.textContent = 'Lightsail 实例名';
|
||||
input.placeholder = '例如: my-lightsail-instance';
|
||||
if (trafficHint) trafficHint.textContent = 'Lightsail: 总流量 (上传+下载) 预警';
|
||||
} else {
|
||||
label.textContent = 'EC2 Instance ID';
|
||||
input.placeholder = '例如: i-0123456789abcdef0';
|
||||
if (trafficHint) trafficHint.textContent = 'EC2: 上传流量预警';
|
||||
}
|
||||
});
|
||||
|
||||
// 切换流量预警字段显示
|
||||
function toggleTrafficAlertFields(prefix) {
|
||||
if (prefix === 'new') {
|
||||
const checkbox = document.getElementById('new_traffic_alert_enabled');
|
||||
const group = document.getElementById('new_traffic_limit_group');
|
||||
group.style.display = checkbox.checked ? 'block' : 'none';
|
||||
} else {
|
||||
const editPanel = document.getElementById(prefix);
|
||||
const checkbox = editPanel.querySelector('[name="traffic_alert_enabled"]');
|
||||
const group = editPanel.querySelector('.traffic-limit-group');
|
||||
if (group) group.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 重置流量预警
|
||||
async function resetTrafficAlert(machineId) {
|
||||
try {
|
||||
const response = await fetch(`/api/machines/${machineId}/reset-alert`, { method: 'POST' });
|
||||
const result = await response.json();
|
||||
if (result.ok) {
|
||||
showToast('success', result.message);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('error', `重置失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
document.getElementById('add-machine-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
name: document.getElementById('new_name').value,
|
||||
aws_service: document.getElementById('new_aws_service').value,
|
||||
aws_region: document.getElementById('new_aws_region').value,
|
||||
instance_id: document.getElementById('new_instance_id').value,
|
||||
note: document.getElementById('new_note').value,
|
||||
enabled: document.getElementById('new_enabled').checked,
|
||||
cf_zone_id: document.getElementById('new_cf_zone_id').value,
|
||||
cf_record_name: document.getElementById('new_cf_record_name').value,
|
||||
cf_proxied: document.getElementById('new_cf_proxied').checked,
|
||||
change_interval_minutes: parseInt(document.getElementById('new_change_interval').value) || 60,
|
||||
auto_enabled: document.getElementById('new_auto_enabled').checked,
|
||||
traffic_alert_enabled: document.getElementById('new_traffic_alert_enabled').checked,
|
||||
traffic_alert_limit_gb: parseFloat(document.getElementById('new_traffic_alert_limit_gb').value) || null,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/machines', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ok) {
|
||||
showToast('success', '节点添加成功!');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
const msg = result.errors ? result.errors.join(', ') : result.message;
|
||||
showToast('error', msg);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('error', `添加失败: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleCollapsible(id) {
|
||||
const section = document.getElementById(id);
|
||||
const content = section.querySelector('.collapsible-content');
|
||||
const isOpen = content.style.display !== 'none';
|
||||
content.style.display = isOpen ? 'none' : 'block';
|
||||
section.classList.toggle('open', !isOpen);
|
||||
}
|
||||
|
||||
function toggleEdit(id) {
|
||||
const editPanel = document.getElementById(`edit-${id}`);
|
||||
editPanel.style.display = editPanel.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function updateMachine(e, id) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const formData = {
|
||||
name: form.querySelector('[name="name"]').value,
|
||||
aws_service: form.querySelector('[name="aws_service"]').value,
|
||||
aws_region: form.querySelector('[name="aws_region"]').value,
|
||||
instance_id: form.querySelector('[name="instance_id"]').value,
|
||||
note: form.querySelector('[name="note"]').value,
|
||||
enabled: form.querySelector('[name="enabled"]').checked,
|
||||
cf_zone_id: form.querySelector('[name="cf_zone_id"]').value,
|
||||
cf_record_name: form.querySelector('[name="cf_record_name"]').value,
|
||||
cf_proxied: form.querySelector('[name="cf_proxied"]').checked,
|
||||
change_interval_minutes: parseInt(form.querySelector('[name="change_interval_minutes"]').value) || 60,
|
||||
auto_enabled: form.querySelector('[name="auto_enabled"]').checked,
|
||||
traffic_alert_enabled: form.querySelector('[name="traffic_alert_enabled"]').checked,
|
||||
traffic_alert_limit_gb: parseFloat(form.querySelector('[name="traffic_alert_limit_gb"]').value) || null,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/machines/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ok) {
|
||||
showToast('success', '节点已更新');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('error', `更新失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMachine(id, name) {
|
||||
if (!confirm(`确定要删除节点 "${name}" 吗?此操作不可恢复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/machines/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ok) {
|
||||
showToast('success', '节点已删除');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('error', `删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user