feat:自动更换ip+流量监控

This commit is contained in:
2026-01-07 17:19:53 +08:00
commit 035da64084
27 changed files with 6182 additions and 0 deletions

427
templates/dashboard.html Normal file
View File

@@ -0,0 +1,427 @@
{% extends "base.html" %}
{% block title %}运行概览 - ProxyAuto Pro{% endblock %}
{% block content %}
<div class="page-header">
<h1>运行概览</h1>
</div>
{% if not machines_status %}
<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>请先前往<a href="{{ url_for('machines') }}">节点管理</a>添加机器</p>
</div>
{% else %}
<!-- 总流量统计 -->
<section class="section">
<h2 class="section-title">总流量统计</h2>
<div class="traffic-summary-grid" id="traffic-summary">
<div class="traffic-summary-card">
<div class="summary-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<span class="summary-title">当月流量</span>
</div>
<div class="summary-metrics">
<div class="summary-metric">
<span class="summary-label">下载</span>
<span class="summary-value" id="month-in">加载中...</span>
</div>
<div class="summary-metric">
<span class="summary-label">上传</span>
<span class="summary-value" id="month-out">加载中...</span>
</div>
<div class="summary-metric total">
<span class="summary-label">总计</span>
<span class="summary-value" id="month-total">加载中...</span>
</div>
</div>
</div>
<div class="traffic-summary-card">
<div class="summary-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
<span class="summary-title">当日流量</span>
</div>
<div class="summary-metrics">
<div class="summary-metric">
<span class="summary-label">下载</span>
<span class="summary-value" id="day-in">加载中...</span>
</div>
<div class="summary-metric">
<span class="summary-label">上传</span>
<span class="summary-value" id="day-out">加载中...</span>
</div>
<div class="summary-metric total">
<span class="summary-label">总计</span>
<span class="summary-value" id="day-total">加载中...</span>
</div>
</div>
</div>
<div class="traffic-summary-card">
<div class="summary-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
<span class="summary-title">历史总流量</span>
</div>
<div class="summary-metrics">
<div class="summary-metric">
<span class="summary-label">下载</span>
<span class="summary-value" id="all-in">加载中...</span>
</div>
<div class="summary-metric">
<span class="summary-label">上传</span>
<span class="summary-value" id="all-out">加载中...</span>
</div>
<div class="summary-metric total">
<span class="summary-label">总计</span>
<span class="summary-value" id="all-total">加载中...</span>
</div>
</div>
</div>
</div>
</section>
<!-- 节点列表 -->
<div class="dashboard-grid">
{% for item in machines_status %}
{% set machine = item.machine %}
{% set scheduler = item.scheduler %}
<div class="machine-dashboard-card">
<div class="card-header">
<div class="card-title-row">
<span class="status-dot {% if machine.enabled %}active{% else %}inactive{% endif %}"></span>
<h3 class="card-title">{{ machine.name }}</h3>
{% if machine.auto_enabled %}
<span class="badge badge-auto">自动</span>
{% endif %}
</div>
<div class="card-subtitle">
{{ machine.aws_service|upper }} | {{ machine.aws_region }}
{% if machine.cf_record_name %}
| {{ machine.cf_record_name }}
{% endif %}
</div>
</div>
<div class="card-metrics">
<div class="card-metric">
<div class="metric-label">当前 IP</div>
<div class="metric-value ip-value">{{ machine.current_ip or '未获取' }}</div>
</div>
<div class="card-metric">
<div class="metric-label">最近更换</div>
<div class="metric-value time-value">
{% if machine.last_run_at %}
{{ machine.last_run_at.strftime('%m-%d %H:%M:%S') }}
{% else %}
--
{% endif %}
</div>
</div>
<div class="card-metric">
<div class="metric-label">更换间隔</div>
<div class="metric-value">{{ machine.change_interval_seconds // 60 }} 分钟</div>
</div>
</div>
<div class="card-traffic" data-machine-id="{{ machine.id }}" data-limit-gb="{{ machine.traffic_alert_limit_gb or 0 }}" data-service="{{ machine.aws_service }}">
<div class="traffic-row">
<div class="traffic-item">
<span class="traffic-label">当月下载</span>
<span class="traffic-value" data-type="in">加载中...</span>
</div>
<div class="traffic-item">
<span class="traffic-label">当月上传</span>
<span class="traffic-value" data-type="out">加载中...</span>
</div>
<div class="traffic-item">
<span class="traffic-label">总流量</span>
<span class="traffic-value" data-type="total">加载中...</span>
</div>
</div>
{% if machine.traffic_alert_enabled %}
<div class="traffic-progress-section">
<div class="traffic-progress-header">
<span class="progress-label">已使用</span>
<span class="progress-percent" data-type="percent">--</span>
</div>
<div class="traffic-progress-bar">
<div class="traffic-progress-fill" data-type="progress" style="width: 0%;"></div>
</div>
<div class="traffic-progress-footer">
<span class="progress-used" data-type="used">--</span>
<span class="progress-limit">/ {{ machine.traffic_alert_limit_gb }} GB</span>
</div>
</div>
{% endif %}
</div>
{% if machine.last_run_at %}
<div class="card-status {% if machine.last_success %}success{% else %}error{% endif %}">
{% if machine.last_success %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span>上次成功</span>
{% else %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span>上次失败</span>
{% endif %}
</div>
{% endif %}
{% if scheduler.next_run_time and scheduler.running %}
<div class="card-countdown" data-next-run="{{ scheduler.next_run_time }}">
<div class="countdown-row">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
<div class="countdown-info">
<div class="countdown-label">距离下次更换</div>
<div class="countdown-value">计算中...</div>
</div>
</div>
<div class="next-time-row">
下次更换: {{ scheduler.next_run_time[:19].replace('T', ' ') }}
</div>
</div>
{% elif not scheduler.running %}
<div class="card-countdown paused">
<div class="countdown-row">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16"/>
<rect x="14" y="4" width="4" height="16"/>
</svg>
<div class="countdown-info">
<div class="countdown-label">自动更换已暂停</div>
<div class="countdown-value">点击"启动"开始</div>
</div>
</div>
</div>
{% endif %}
<div class="card-actions">
<button class="btn btn-primary btn-action" onclick="runIpChange({{ machine.id }}, '{{ machine.name }}', this)">
<svg class="btn-icon-normal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
<svg class="btn-icon-loading spinner" viewBox="0 0 24 24" style="display: none;">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" fill="none" stroke-dasharray="32" stroke-linecap="round"/>
</svg>
<span class="btn-text">更换 IP</span>
</button>
<button class="btn btn-secondary btn-action" onclick="toggleAuto({{ machine.id }}, this)">
{% if scheduler.running %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16"/>
<rect x="14" y="4" width="4" height="16"/>
</svg>
<span>暂停</span>
{% else %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
<span>启动</span>
{% endif %}
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
// 页面加载后启动倒计时和流量加载
document.addEventListener('DOMContentLoaded', function() {
startCountdownUpdater();
loadAllTrafficData();
loadTrafficSummary();
});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + units[i];
}
async function loadTrafficSummary() {
try {
const response = await fetch('/api/traffic/summary');
const result = await response.json();
if (result.ok) {
// 当月流量
document.getElementById('month-in').textContent = result.month.network_in_formatted || formatBytes(result.month.network_in);
document.getElementById('month-out').textContent = result.month.network_out_formatted || formatBytes(result.month.network_out);
document.getElementById('month-total').textContent = result.month.total_formatted || formatBytes(result.month.total);
// 当日流量
document.getElementById('day-in').textContent = result.day.network_in_formatted || formatBytes(result.day.network_in);
document.getElementById('day-out').textContent = result.day.network_out_formatted || formatBytes(result.day.network_out);
document.getElementById('day-total').textContent = result.day.total_formatted || formatBytes(result.day.total);
// 历史总流量
document.getElementById('all-in').textContent = result.all_time.network_in_formatted || formatBytes(result.all_time.network_in);
document.getElementById('all-out').textContent = result.all_time.network_out_formatted || formatBytes(result.all_time.network_out);
document.getElementById('all-total').textContent = result.all_time.total_formatted || formatBytes(result.all_time.total);
} else {
setTrafficSummaryError('加载失败');
}
} catch (error) {
setTrafficSummaryError('--');
}
}
function setTrafficSummaryError(text) {
['month-in', 'month-out', 'month-total', 'day-in', 'day-out', 'day-total', 'all-in', 'all-out', 'all-total'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = text;
});
}
async function loadAllTrafficData() {
const trafficCards = document.querySelectorAll('.card-traffic');
trafficCards.forEach(card => {
const machineId = card.dataset.machineId;
loadTrafficData(machineId, card);
});
}
async function loadTrafficData(machineId, card) {
const limitGb = parseFloat(card.dataset.limitGb) || 0;
const service = card.dataset.service;
try {
const response = await fetch(`/api/traffic/${machineId}/current`);
const result = await response.json();
if (result.ok) {
card.querySelector('[data-type="in"]').textContent = formatBytes(result.network_in);
card.querySelector('[data-type="out"]').textContent = formatBytes(result.network_out);
card.querySelector('[data-type="total"]').textContent = formatBytes(result.total);
// 计算百分比
if (limitGb > 0) {
// Lightsail 用总流量EC2 用上传流量
const usedBytes = service === 'lightsail' ? result.total : result.network_out;
const usedGb = usedBytes / (1024 * 1024 * 1024);
const percent = Math.min((usedGb / limitGb) * 100, 100);
const percentEl = card.querySelector('[data-type="percent"]');
const progressEl = card.querySelector('[data-type="progress"]');
const usedEl = card.querySelector('[data-type="used"]');
if (percentEl) percentEl.textContent = percent.toFixed(2) + '%';
if (progressEl) {
progressEl.style.width = percent + '%';
// 根据百分比设置颜色
if (percent >= 90) {
progressEl.classList.add('danger');
progressEl.classList.remove('warning');
} else if (percent >= 70) {
progressEl.classList.add('warning');
progressEl.classList.remove('danger');
} else {
progressEl.classList.remove('warning', 'danger');
}
}
if (usedEl) usedEl.textContent = usedGb.toFixed(2) + ' GB';
}
} else {
card.querySelector('[data-type="in"]').textContent = '--';
card.querySelector('[data-type="out"]').textContent = '--';
card.querySelector('[data-type="total"]').textContent = '--';
}
} catch (error) {
card.querySelector('[data-type="in"]').textContent = '--';
card.querySelector('[data-type="out"]').textContent = '--';
card.querySelector('[data-type="total"]').textContent = '--';
}
}
async function runIpChange(machineId, machineName, btn) {
btn.disabled = true;
const normalIcon = btn.querySelector('.btn-icon-normal');
const loadingIcon = btn.querySelector('.btn-icon-loading');
const btnText = btn.querySelector('.btn-text');
normalIcon.style.display = 'none';
loadingIcon.style.display = 'block';
btnText.textContent = '执行中...';
try {
const result = await withMinLoadTime(
fetch(`/api/run-ip-change/${machineId}`, { method: 'POST' }).then(r => r.json()),
800
);
if (result.ok) {
showToast('success', `${machineName}: 新 IP ${result.public_ip}`);
setTimeout(() => location.reload(), 1500);
} else {
showToast('error', `${machineName}: ${result.message}`);
}
} catch (error) {
showToast('error', `请求失败: ${error.message}`);
} finally {
btn.disabled = false;
normalIcon.style.display = 'block';
loadingIcon.style.display = 'none';
btnText.textContent = '更换 IP';
}
}
async function toggleAuto(machineId, btn) {
btn.disabled = true;
try {
const result = await withMinLoadTime(
fetch(`/api/toggle-auto/${machineId}`, { method: 'POST' }).then(r => r.json()),
500
);
if (result.ok) {
showToast('success', result.message);
setTimeout(() => location.reload(), 1000);
} else {
showToast('error', result.message);
}
} catch (error) {
showToast('error', `请求失败: ${error.message}`);
} finally {
btn.disabled = false;
}
}
</script>
{% endblock %}