Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
918fcb051c
|
|||
|
acbe3597d6
|
|||
|
3566c6b2da
|
|||
|
8b044f6dab
|
|||
|
4c4b963aef
|
|||
|
38ac972960
|
|||
|
becd3aa15d
|
|||
|
ac907308c0
|
|||
|
fa2fe6a15d
|
|||
|
927255f44c
|
|||
|
0318cb7e4a
|
20
.github/workflows/bearer-pr.yml
vendored
Normal file
20
.github/workflows/bearer-pr.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Bearer PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
rule_check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Bearer
|
||||
uses: bearer/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
|
||||
with:
|
||||
diff: true
|
||||
29
.github/workflows/bearer.yml
vendored
Normal file
29
.github/workflows/bearer.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Bearer Master
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
rule_check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Bearer
|
||||
uses: bearer/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2
|
||||
with:
|
||||
format: sarif
|
||||
output: results.sarif
|
||||
|
||||
- name: Upload SARIF file
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@2827891b2e5e0510dceab8c3619f4fe255451277 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: bearer-security-scan
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ node_modules
|
||||
|
||||
# local storage
|
||||
storage/
|
||||
|
||||
*.pyc
|
||||
@@ -25,7 +25,11 @@ WORKDIR /app
|
||||
|
||||
# Install Python deps
|
||||
COPY ./requirements.txt .
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev && \
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
musl-dev \
|
||||
linux-headers \
|
||||
python3-dev && \
|
||||
pip install -r requirements.txt && \
|
||||
apk del .build-deps
|
||||
|
||||
|
||||
77
meshchat.py
77
meshchat.py
@@ -22,6 +22,7 @@ import webbrowser
|
||||
|
||||
from peewee import SqliteDatabase
|
||||
from serial.tools import list_ports
|
||||
import psutil
|
||||
|
||||
import database
|
||||
from src.backend.announce_handler import AnnounceHandler
|
||||
@@ -147,6 +148,12 @@ class ReticulumMeshChat:
|
||||
# remember websocket clients
|
||||
self.websocket_clients: List[web.WebSocketResponse] = []
|
||||
|
||||
# track announce timestamps for rate calculation
|
||||
self.announce_timestamps = []
|
||||
|
||||
# track download speeds for nomadnetwork files (list of tuples: (file_size_bytes, duration_seconds))
|
||||
self.download_speeds = []
|
||||
|
||||
# register audio call identity
|
||||
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
||||
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
||||
@@ -954,6 +961,34 @@ class ReticulumMeshChat:
|
||||
# get app info
|
||||
@routes.get("/api/v1/app/info")
|
||||
async def index(request):
|
||||
# Get memory usage for current process
|
||||
process = psutil.Process()
|
||||
memory_info = process.memory_info()
|
||||
|
||||
# Get network I/O statistics
|
||||
net_io = psutil.net_io_counters()
|
||||
|
||||
# Get total paths
|
||||
path_table = self.reticulum.get_path_table()
|
||||
total_paths = len(path_table)
|
||||
|
||||
# Calculate announce rates
|
||||
current_time = time.time()
|
||||
announces_per_second = len([t for t in self.announce_timestamps if current_time - t <= 1.0])
|
||||
announces_per_minute = len([t for t in self.announce_timestamps if current_time - t <= 60.0])
|
||||
announces_per_hour = len([t for t in self.announce_timestamps if current_time - t <= 3600.0])
|
||||
|
||||
# Clean up old announce timestamps (older than 1 hour)
|
||||
self.announce_timestamps = [t for t in self.announce_timestamps if current_time - t <= 3600.0]
|
||||
|
||||
# Calculate average download speed
|
||||
avg_download_speed_bps = None
|
||||
if self.download_speeds:
|
||||
total_bytes = sum(size for size, _ in self.download_speeds)
|
||||
total_duration = sum(duration for _, duration in self.download_speeds)
|
||||
if total_duration > 0:
|
||||
avg_download_speed_bps = total_bytes / total_duration
|
||||
|
||||
return web.json_response({
|
||||
"app_info": {
|
||||
"version": self.get_app_version(),
|
||||
@@ -966,6 +1001,25 @@ class ReticulumMeshChat:
|
||||
"reticulum_config_path": self.reticulum.configpath,
|
||||
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
||||
"memory_usage": {
|
||||
"rss": memory_info.rss, # Resident Set Size (bytes)
|
||||
"vms": memory_info.vms, # Virtual Memory Size (bytes)
|
||||
},
|
||||
"network_stats": {
|
||||
"bytes_sent": net_io.bytes_sent,
|
||||
"bytes_recv": net_io.bytes_recv,
|
||||
"packets_sent": net_io.packets_sent,
|
||||
"packets_recv": net_io.packets_recv,
|
||||
},
|
||||
"reticulum_stats": {
|
||||
"total_paths": total_paths,
|
||||
"announces_per_second": announces_per_second,
|
||||
"announces_per_minute": announces_per_minute,
|
||||
"announces_per_hour": announces_per_hour,
|
||||
},
|
||||
"download_stats": {
|
||||
"avg_download_speed_bps": avg_download_speed_bps,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2245,6 +2299,16 @@ class ReticulumMeshChat:
|
||||
|
||||
# handle successful file download
|
||||
def on_file_download_success(file_name, file_bytes):
|
||||
# Track download speed
|
||||
download_size = len(file_bytes)
|
||||
if hasattr(downloader, 'start_time') and downloader.start_time:
|
||||
download_duration = time.time() - downloader.start_time
|
||||
if download_duration > 0:
|
||||
self.download_speeds.append((download_size, download_duration))
|
||||
# Keep only last 100 downloads for average calculation
|
||||
if len(self.download_speeds) > 100:
|
||||
self.download_speeds.pop(0)
|
||||
|
||||
AsyncUtils.run_async(client.send_str(json.dumps({
|
||||
"type": "nomadnet.file.download",
|
||||
"nomadnet_file_download": {
|
||||
@@ -2282,6 +2346,7 @@ class ReticulumMeshChat:
|
||||
|
||||
# download the file
|
||||
downloader = NomadnetFileDownloader(destination_hash, file_path, on_file_download_success, on_file_download_failure, on_file_download_progress)
|
||||
downloader.start_time = time.time()
|
||||
AsyncUtils.run_async(downloader.download())
|
||||
|
||||
# handle downloading a page from a nomadnet node
|
||||
@@ -3054,6 +3119,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3075,6 +3143,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3100,6 +3171,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3184,6 +3258,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
|
||||
1796
package-lock.json
generated
1796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reticulum-meshchat",
|
||||
"version": "2.2.1",
|
||||
"version": "2.32.0",
|
||||
"description": "",
|
||||
"main": "electron/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,5 +2,6 @@ aiohttp>=3.12.14
|
||||
cx_freeze>=7.0.0
|
||||
lxmf>=0.9.2
|
||||
peewee>=3.18.1
|
||||
rns>=1.0.1
|
||||
psutil>=7.1.3
|
||||
rns>=1.0.2
|
||||
websockets>=14.2
|
||||
|
||||
@@ -64,6 +64,141 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- system resources -->
|
||||
<div v-if="appInfo && appInfo.memory_usage" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
System Resources
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- memory usage -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Memory Usage (RSS)</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.rss) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- virtual memory -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Virtual Memory Size</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.vms) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- network statistics -->
|
||||
<div v-if="appInfo && appInfo.network_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Network Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- bytes sent -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Data Sent</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_sent) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- bytes received -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Data Received</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_recv) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- packets sent -->
|
||||
<div class="p-1">
|
||||
<div>Packets Sent</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_sent) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- packets received -->
|
||||
<div class="p-1">
|
||||
<div>Packets Received</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_recv) }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reticulum statistics -->
|
||||
<div v-if="appInfo && appInfo.reticulum_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Reticulum Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- total paths -->
|
||||
<div class="p-1">
|
||||
<div>Total Paths</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.total_paths) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per second -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Second</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_second) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per minute -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Minute</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_minute) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per hour -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Hour</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_hour) }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- download statistics -->
|
||||
<div v-if="appInfo && appInfo.download_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Download Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- average download speed -->
|
||||
<div class="p-1">
|
||||
<div>Average Download Speed</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
||||
<span v-if="appInfo.download_stats.avg_download_speed_bps !== null">
|
||||
{{ formatBytesPerSecond(appInfo.download_stats.avg_download_speed_bps) }}
|
||||
</span>
|
||||
<span v-else>No downloads yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reticulum status -->
|
||||
<div v-if="appInfo" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">Reticulum Status</div>
|
||||
@@ -126,11 +261,21 @@ export default {
|
||||
return {
|
||||
appInfo: null,
|
||||
config: null,
|
||||
updateInterval: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getAppInfo();
|
||||
this.getConfig();
|
||||
// Update stats every 5 seconds
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.getAppInfo();
|
||||
}, 5000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getAppInfo() {
|
||||
@@ -166,6 +311,12 @@ export default {
|
||||
formatBytes: function(bytes) {
|
||||
return Utils.formatBytes(bytes);
|
||||
},
|
||||
formatNumber: function(num) {
|
||||
return Utils.formatNumber(num);
|
||||
},
|
||||
formatBytesPerSecond: function(bytesPerSecond) {
|
||||
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isElectron() {
|
||||
|
||||
@@ -124,7 +124,12 @@
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="my-auto">Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)</div>
|
||||
<div class="my-auto">
|
||||
Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
|
||||
<span v-if="nodeFileDownloadSpeed !== null" class="ml-2 text-sm">
|
||||
- {{ formatBytesPerSecond(nodeFileDownloadSpeed) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,6 +184,7 @@ import DialogUtils from "../../js/DialogUtils";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||
import Utils from "../../js/Utils";
|
||||
|
||||
export default {
|
||||
name: 'NomadNetworkPage',
|
||||
@@ -213,6 +219,10 @@ export default {
|
||||
isDownloadingNodeFile: false,
|
||||
nodeFilePath: null,
|
||||
nodeFileProgress: 0,
|
||||
nodeFileDownloadStartTime: null,
|
||||
nodeFileLastProgressTime: null,
|
||||
nodeFileLastProgressValue: 0,
|
||||
nodeFileDownloadSpeed: null,
|
||||
|
||||
nomadnetPageDownloadCallbacks: {},
|
||||
nomadnetFileDownloadCallbacks: {},
|
||||
@@ -756,26 +766,72 @@ export default {
|
||||
this.isDownloadingNodeFile = true;
|
||||
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
||||
this.nodeFileProgress = 0;
|
||||
this.nodeFileDownloadStartTime = Date.now();
|
||||
this.nodeFileLastProgressTime = Date.now();
|
||||
this.nodeFileLastProgressValue = 0;
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
|
||||
// start file download
|
||||
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
||||
|
||||
// Calculate final download speed based on actual file size
|
||||
if (this.nodeFileDownloadStartTime) {
|
||||
const totalTime = (Date.now() - this.nodeFileDownloadStartTime) / 1000; // seconds
|
||||
const fileSizeBytes = atob(fileBytesBase64).length;
|
||||
if (totalTime > 0) {
|
||||
this.nodeFileDownloadSpeed = fileSizeBytes / totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
// no longer downloading
|
||||
this.isDownloadingNodeFile = false;
|
||||
|
||||
// download file to browser
|
||||
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
||||
|
||||
// Clear speed after a moment
|
||||
setTimeout(() => {
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
}, 2000);
|
||||
|
||||
}, (failureReason) => {
|
||||
|
||||
// no longer downloading
|
||||
this.isDownloadingNodeFile = false;
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
|
||||
// show error message
|
||||
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
||||
|
||||
}, (progress) => {
|
||||
this.nodeFileProgress = Math.round(progress * 100);
|
||||
const currentTime = Date.now();
|
||||
const progressValue = progress;
|
||||
this.nodeFileProgress = Math.round(progressValue * 100);
|
||||
|
||||
// Calculate estimated download speed based on progress rate
|
||||
if (this.nodeFileDownloadStartTime && progressValue > 0) {
|
||||
const elapsedTime = (currentTime - this.nodeFileDownloadStartTime) / 1000; // seconds
|
||||
if (elapsedTime > 0.5) { // Only calculate after at least 0.5 seconds
|
||||
// Estimate total file size based on progress rate
|
||||
// If we've downloaded progressValue in elapsedTime, estimate total time
|
||||
const estimatedTotalTime = elapsedTime / progressValue;
|
||||
// Estimate file size based on average download speed assumption
|
||||
// We'll refine this when download completes with actual size
|
||||
// For now, estimate based on typical mesh network file sizes (100KB-10MB range)
|
||||
// Use a conservative estimate that will be updated when download completes
|
||||
const estimatedFileSize = 500 * 1024; // Start with 500KB estimate
|
||||
const estimatedBytesDownloaded = estimatedFileSize * progressValue;
|
||||
const estimatedSpeed = estimatedBytesDownloaded / elapsedTime;
|
||||
|
||||
// Only update if we have a reasonable estimate
|
||||
if (estimatedSpeed > 0 && estimatedSpeed < 100 * 1024 * 1024) { // Cap at 100MB/s
|
||||
this.nodeFileDownloadSpeed = estimatedSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeFileLastProgressTime = currentTime;
|
||||
this.nodeFileLastProgressValue = progressValue;
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -829,6 +885,9 @@ export default {
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||
|
||||
},
|
||||
formatBytesPerSecond: function(bytesPerSecond) {
|
||||
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||
},
|
||||
onNodeClick: function(node) {
|
||||
|
||||
// update selected node
|
||||
|
||||
@@ -25,6 +25,13 @@ class Utils {
|
||||
|
||||
}
|
||||
|
||||
static formatNumber(num) {
|
||||
if(num === 0){
|
||||
return '0';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
static parseSeconds(secondsToFormat) {
|
||||
secondsToFormat = Number(secondsToFormat);
|
||||
var days = Math.floor(secondsToFormat / (3600 * 24));
|
||||
@@ -127,6 +134,22 @@ class Utils {
|
||||
|
||||
}
|
||||
|
||||
static formatBytesPerSecond(bytesPerSecond) {
|
||||
|
||||
if(bytesPerSecond === 0 || bytesPerSecond == null){
|
||||
return '0 B/s';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const decimals = 1;
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
|
||||
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
|
||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
|
||||
}
|
||||
|
||||
static formatFrequency(hz) {
|
||||
|
||||
if(hz === 0 || hz == null){
|
||||
|
||||
Reference in New Issue
Block a user