Merge 1bca7bec5f into 2cdc0265b8
commit
b7336df41e
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue