feat:自动更换ip+流量监控
This commit is contained in:
427
templates/dashboard.html
Normal file
427
templates/dashboard.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user