pull/1137/merge
Murtazin Aizat 2026-02-12 15:28:04 +03:00 committed by GitHub
commit b7336df41e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 394 additions and 9 deletions

View File

@ -526,6 +526,13 @@ def API_getWireguardConfigurationRealtimeTraffic():
return ResponseObject(False, "Configuration does not exist", status_code=404)
return ResponseObject(data=WireguardConfigurations[configurationName].getRealtimeTrafficUsage())
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationPeersRealtimeSpeed')
def API_getWireguardConfigurationPeersRealtimeSpeed():
configurationName = request.args.get('configurationName')
if configurationName is None or configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist", status_code=404)
return ResponseObject(data=WireguardConfigurations[configurationName].getPeersRealtimeSpeed())
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationBackup')
def API_getWireguardConfigurationBackup():
configurationName = request.args.get('configurationName')

View File

@ -66,6 +66,8 @@ class WireguardConfiguration:
self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf')
self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString("wgdashboard"))
self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData()
self.__last_transfer: dict = {}
self.__last_transfer_time: float = 0
self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1]
if name is not None:
@ -1155,7 +1157,70 @@ class WireguardConfiguration:
return { "sent": 0, "recv": 0 }
else:
return { "sent": 0, "recv": 0 }
def getPeersRealtimeSpeed(self):
"""
Calculate per-peer bandwidth speed (MB/s) by comparing two successive
'wg show <interface> transfer' snapshots taken ~1 second apart.
Returns dict: { peer_id: { "name": str, "sent": float, "recv": float } }
"""
if not self.getStatus():
return {}
def _parse_transfer(raw: str) -> dict:
result = {}
for line in raw.strip().split("\n"):
parts = line.split("\t")
if len(parts) == 3:
peer_id = parts[0]
recv_bytes = int(parts[1])
sent_bytes = int(parts[2])
result[peer_id] = {"recv": recv_bytes, "sent": sent_bytes}
return result
try:
snap1_raw = subprocess.check_output(
f"{self.Protocol} show {self.Name} transfer",
shell=True, stderr=subprocess.STDOUT
).decode("UTF-8")
snap1 = _parse_transfer(snap1_raw)
time.sleep(1)
snap2_raw = subprocess.check_output(
f"{self.Protocol} show {self.Name} transfer",
shell=True, stderr=subprocess.STDOUT
).decode("UTF-8")
snap2 = _parse_transfer(snap2_raw)
except subprocess.CalledProcessError:
return {}
peers_speed = {}
for peer_id in snap2:
if peer_id in snap1:
delta_recv = snap2[peer_id]["recv"] - snap1[peer_id]["recv"]
delta_sent = snap2[peer_id]["sent"] - snap1[peer_id]["sent"]
# Convert bytes to MB/s
speed_recv = round(max(delta_recv, 0) / (1024 * 1024), 4)
speed_sent = round(max(delta_sent, 0) / (1024 * 1024), 4)
else:
speed_recv = 0
speed_sent = 0
# Find peer name
peer_name = ""
found, p = self.searchPeer(peer_id)
if found:
peer_name = p.name or ""
peers_speed[peer_id] = {
"name": peer_name,
"recv": speed_recv,
"sent": speed_sent
}
return peers_speed
'''
Manager WireGuard Configuration Information
'''

View File

@ -15,7 +15,7 @@ export default {
PeerTagBadge, LocaleText, PeerSettingsDropdown
},
props: {
Peer: Object, ConfigurationInfo: Object, order: Number, searchPeersLength: Number
Peer: Object, ConfigurationInfo: Object, order: Number, searchPeersLength: Number, peerSpeed: Object
},
setup(){
const target = ref(null);
@ -58,7 +58,7 @@ export default {
</div>
<div style="font-size: 0.8rem" class="ms-auto d-flex gap-2">
<div style="font-size: 0.8rem" class="ms-auto d-flex gap-2 flex-wrap justify-content-end">
<span class="text-primary">
<i class="bi bi-arrow-down"></i><strong>
{{(Peer.cumu_receive + Peer.total_receive).toFixed(4)}}</strong> GB
@ -71,6 +71,13 @@ export default {
<i class="bi bi-arrows-angle-contract"></i>
{{getLatestHandshake}} ago
</span>
<span class="badge bg-info-subtle text-info-emphasis d-flex align-items-center gap-1"
v-if="peerSpeed && (peerSpeed.recv > 0 || peerSpeed.sent > 0)">
<i class="bi bi-speedometer2"></i>
<span class="text-primary">{{peerSpeed.recv}} </span>
<span class="text-success">{{peerSpeed.sent}} </span>
MB/s
</span>
</div>
</div>
<div v-else class="border-0 card-header bg-transparent text-warning fw-bold"

View File

@ -102,18 +102,39 @@ const fetchPeerList = async () => {
}
await fetchPeerList()
// Per-Peer Realtime Speed =====================================
const peerSpeeds = ref({})
const fetchPeerSpeeds = async () => {
if (!configurationInfo.value.Status) return
await fetchGet("/api/getWireguardConfigurationPeersRealtimeSpeed", {
configurationName: route.params.id
}, (res) => {
if (res.status && res.data) {
peerSpeeds.value = res.data
}
})
}
// Fetch Peer Interval =====================================
const fetchPeerListInterval = ref(undefined)
const fetchPeerSpeedInterval = ref(undefined)
const setFetchPeerListInterval = () => {
clearInterval(fetchPeerListInterval.value)
clearInterval(fetchPeerSpeedInterval.value)
const interval = parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval)
fetchPeerListInterval.value = setInterval(async () => {
await fetchPeerList()
}, parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval))
}, interval)
fetchPeerSpeedInterval.value = setInterval(async () => {
await fetchPeerSpeeds()
}, interval)
}
setFetchPeerListInterval()
onBeforeUnmount(() => {
clearInterval(fetchPeerListInterval.value);
clearInterval(fetchPeerSpeedInterval.value);
fetchPeerListInterval.value = undefined;
fetchPeerSpeedInterval.value = undefined;
wireguardConfigurationStore.Filter.HiddenTags = []
})
@ -414,10 +435,11 @@ watch(() => route.query.id, (newValue) => {
:class="{'col-lg-6 col-xl-4': dashboardStore.Configuration.Server.dashboard_peer_list_display === 'grid'}"
:key="peer.id"
v-for="(peer, order) in searchPeers">
<Peer :Peer="peer"
:searchPeersLength="searchPeers.length"
:order="order"
:ConfigurationInfo="configurationInfo"
<Peer :Peer="peer"
:searchPeersLength="searchPeers.length"
:order="order"
:ConfigurationInfo="configurationInfo"
:peerSpeed="peerSpeeds[peer.id]"
@details="configurationModals.peerDetails.modalOpen = true; configurationModalSelectedPeer = peer"
@share="configurationModals.peerShare.modalOpen = true; configurationModalSelectedPeer = peer"
@refresh="fetchPeerList()"

View File

@ -49,6 +49,17 @@ const historyReceivedData = ref({
timestamp: [],
data: []
})
// Per-peer speed history: { peerId: { name, timestamps: [], recv: [], sent: [] } }
const peerSpeedHistory = ref({})
const peerSpeedColors = [
'#0d6efd', '#198754', '#ffc107', '#dc3545', '#6f42c1',
'#20c997', '#fd7e14', '#0dcaf0', '#d63384', '#6610f2',
'#198754', '#e83e8c', '#17a2b8', '#28a745', '#007bff'
]
// Current per-peer speed snapshot
const currentPeerSpeed = ref({})
const route = useRoute()
const dashboardStore = DashboardConfigurationStore()
const fetchRealtimeTrafficInterval = ref(undefined)
@ -74,13 +85,54 @@ const fetchRealtimeTraffic = async () => {
}
})
}
const fetchPeerSpeedInterval = ref(undefined)
const MAX_PEER_SPEED_POINTS = 30
const fetchPeerSpeed = async () => {
await fetchGet("/api/getWireguardConfigurationPeersRealtimeSpeed", {
configurationName: route.params.id
}, (res) => {
if (!res.status || !res.data) return
const timestamp = dayjs().format("hh:mm:ss A")
currentPeerSpeed.value = res.data
for (const [peerId, info] of Object.entries(res.data)) {
if (!peerSpeedHistory.value[peerId]) {
peerSpeedHistory.value[peerId] = {
name: info.name || peerId.substring(0, 8) + '...',
timestamps: [],
recv: [],
sent: []
}
}
const h = peerSpeedHistory.value[peerId]
h.name = info.name || peerId.substring(0, 8) + '...'
h.timestamps.push(timestamp)
h.recv.push(info.recv)
h.sent.push(info.sent)
// Trim to max points
if (h.timestamps.length > MAX_PEER_SPEED_POINTS) {
h.timestamps.shift()
h.recv.shift()
h.sent.shift()
}
}
})
}
const toggleFetchRealtimeTraffic = () => {
clearInterval(fetchRealtimeTrafficInterval.value)
clearInterval(fetchPeerSpeedInterval.value)
fetchRealtimeTrafficInterval.value = undefined;
fetchPeerSpeedInterval.value = undefined;
if (props.configurationInfo.Status){
const interval = parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval)
fetchRealtimeTrafficInterval.value = setInterval(() => {
fetchRealtimeTraffic()
}, parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval))
}, interval)
fetchPeerSpeedInterval.value = setInterval(() => {
fetchPeerSpeed()
}, interval)
}
}
@ -98,8 +150,11 @@ watch(() => dashboardStore.Configuration.Server.dashboard_refresh_interval, () =
onBeforeUnmount(() => {
clearInterval(fetchRealtimeTrafficInterval.value)
clearInterval(fetchPeerSpeedInterval.value)
fetchRealtimeTrafficInterval.value = undefined;
fetchPeerSpeedInterval.value = undefined;
})
const peersDataUsageChartData = computed(() => {
let data = props.configurationPeers.filter(x => (x.cumu_data + x.total_data) > 0)
@ -122,6 +177,90 @@ const peersDataUsageChartData = computed(() => {
}]
}
})
// Per-peer current speed bar chart
const peerSpeedBarData = computed(() => {
const entries = Object.entries(currentPeerSpeed.value)
if (entries.length === 0) return { labels: [], datasets: [] }
const labels = entries.map(([id, info]) => info.name || id.substring(0, 8) + '...')
const recvData = entries.map(([id, info]) => info.recv)
const sentData = entries.map(([id, info]) => info.sent)
return {
labels,
datasets: [
{
label: GetLocale('Received'),
data: recvData,
backgroundColor: '#0d6efd90',
borderColor: '#0d6efd',
borderWidth: 1,
},
{
label: GetLocale('Sent'),
data: sentData,
backgroundColor: '#19875490',
borderColor: '#198754',
borderWidth: 1,
}
]
}
})
// Per-peer speed line chart - Received
const peerSpeedRecvLineData = computed(() => {
const peerIds = Object.keys(peerSpeedHistory.value)
if (peerIds.length === 0) return { labels: [], datasets: [] }
// Use timestamps from the first peer (all should be in sync)
const firstPeer = peerSpeedHistory.value[peerIds[0]]
const labels = firstPeer ? [...firstPeer.timestamps] : []
const datasets = peerIds.map((peerId, idx) => {
const h = peerSpeedHistory.value[peerId]
const color = peerSpeedColors[idx % peerSpeedColors.length]
return {
label: h.name,
data: [...h.recv],
borderColor: color,
backgroundColor: color + '40',
fill: false,
tension: 0.2,
pointRadius: 1,
borderWidth: 2,
}
})
return { labels, datasets }
})
// Per-peer speed line chart - Sent
const peerSpeedSentLineData = computed(() => {
const peerIds = Object.keys(peerSpeedHistory.value)
if (peerIds.length === 0) return { labels: [], datasets: [] }
const firstPeer = peerSpeedHistory.value[peerIds[0]]
const labels = firstPeer ? [...firstPeer.timestamps] : []
const datasets = peerIds.map((peerId, idx) => {
const h = peerSpeedHistory.value[peerId]
const color = peerSpeedColors[idx % peerSpeedColors.length]
return {
label: h.name,
data: [...h.sent],
borderColor: color,
backgroundColor: color + '40',
fill: false,
tension: 0.2,
pointRadius: 1,
borderWidth: 2,
}
})
return { labels, datasets }
})
const peersRealtimeSentData = computed(() => {
return {
labels: [...historySentData.value.timestamp],
@ -188,6 +327,47 @@ const peersDataUsageChartOption = computed(() => {
}
}
})
const peerSpeedBarOption = computed(() => {
return {
responsive: true,
plugins: {
legend: {
display: true,
position: 'top',
labels: { boxWidth: 12 }
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.dataset.label}: ${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val) => {
return `${Math.round((val + Number.EPSILON) * 1000) / 1000} MB/s`
}
},
grid: {
display: false
},
}
}
}
})
const realtimePeersChartOption = computed(() => {
return {
responsive: true,
@ -225,6 +405,57 @@ const realtimePeersChartOption = computed(() => {
}
}
})
const perPeerLineChartOption = computed(() => {
return {
responsive: true,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
boxWidth: 12,
font: { size: 10 }
}
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.dataset.label}: ${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: true
},
},
y: {
ticks: {
callback: (val) => {
return `${Math.round((val + Number.EPSILON) * 1000) / 1000} MB/s`
}
},
grid: {
display: true
},
}
}
}
})
const hasPeerSpeedData = computed(() => {
return Object.keys(currentPeerSpeed.value).length > 0
})
</script>
<template>
@ -281,6 +512,59 @@ const realtimePeersChartOption = computed(() => {
</div>
</div>
</div>
<!-- Per-Peer Bandwidth Monitoring -->
<div class="col-12" v-if="hasPeerSpeedData">
<div class="card rounded-3 bg-transparent" style="height: 300px">
<div class="card-header bg-transparent border-0">
<small class="text-muted">
<i class="bi bi-speedometer2 me-1"></i>
<LocaleText t="Per-Peer Current Speed"></LocaleText>
</small>
</div>
<div class="card-body pt-1">
<Bar
:data="peerSpeedBarData"
:options="peerSpeedBarOption"
style="width: 100%; height: 220px; max-height: 220px"
></Bar>
</div>
</div>
</div>
<div class="col-sm col-lg-6" v-if="hasPeerSpeedData">
<div class="card rounded-3 bg-transparent" style="height: 300px">
<div class="card-header bg-transparent border-0 d-flex align-items-center">
<small class="text-muted">
<i class="bi bi-arrow-down me-1"></i>
<LocaleText t="Per-Peer Received Speed"></LocaleText>
</small>
</div>
<div class="card-body pt-1">
<Line
:options="perPeerLineChartOption"
:data="peerSpeedRecvLineData"
style="width: 100%; height: 220px; max-height: 220px"
></Line>
</div>
</div>
</div>
<div class="col-sm col-lg-6" v-if="hasPeerSpeedData">
<div class="card rounded-3 bg-transparent" style="height: 300px">
<div class="card-header bg-transparent border-0 d-flex align-items-center">
<small class="text-muted">
<i class="bi bi-arrow-up me-1"></i>
<LocaleText t="Per-Peer Sent Speed"></LocaleText>
</small>
</div>
<div class="card-body pt-1">
<Line
:options="perPeerLineChartOption"
:data="peerSpeedSentLineData"
style="width: 100%; height: 220px; max-height: 220px"
></Line>
</div>
</div>
</div>
</div>
</template>