Files
ProxyAuto/templates/dashboard.html

428 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}