<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<style type="text/css">
html, body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.server-container {
align-items: start;
display: flex;
justify-content: center;
overflow-y: auto;
}
.server {
border-spacing: 2px 4px;
background-color: #eee;
flex: 1;
white-space: nowrap;
max-width: 360px;
margin: 3px;
}
.server-offline {
filter: brightness(50%);
}
.first-row {
font-weight: bold;
}
.server-ip {
text-align: center;
font-size: 150%;
}
.client-label, .client-speed {
border: 1px solid #888;
text-align: center;
text-shadow: #fff 1.2px 1.2px;
}
.expand {
width: 99%;
}
</style>
</head>
<body>
<div id="app">
<div class="server-container">
<table v-for="server in servers" class="server" :class="{ 'server-offline': server.offline }" :style="{ border: '4px solid ' + server.graphColor }">
<tbody>
<tr class="first-row"><td colspan="2" class="server-ip">{{ server.address }}</td></tr>
<tr><td>Uptime:</td><td class="expand">{{ formatSeconds(server.uptime) }}</td></tr>
<tr>
<td>Upload speed:</td>
<td class="client-speed" :style="calcBackgroundStyle(peak ? server.peakUploadSpeed : server.avgUploadSpeed)">
<span>{{ formatBytes(peak ? server.peakUploadSpeed : server.avgUploadSpeed) }}/s</span>
</td>
</tr>
<tr>
<td>Download speed:</td>
<td class="client-speed" :style="calcBackgroundStyle(peak ? server.peakDownloadSpeed : server.avgDownloadSpeed)">
<span>{{ formatBytes(peak ? server.peakDownloadSpeed : server.avgDownloadSpeed) }}/s</span>
</td>
</tr>
<tr><td>Total sent:</td><td>{{ formatBytes(server.bytesSent) }}</td></tr>
<tr><td>Total received:</td><td>{{ formatBytes(server.bytesReceived) }}</td></tr>
<tr><td>Client count:</td><td>{{ server.clientCount }}</td></tr>
<tr><td>Server count:</td><td>{{ server.serverCount }}</td></tr>
<tr v-for="client in server.clients">
<td class="client-label">{{ formatIpAddress(client.address) }}</td>
<td class="client-speed" :style="calcBackgroundStyle(peak ? client.peakUploadSpeed : client.avgUploadSpeed)">
<span>{{ formatBytes(peak ? client.peakUploadSpeed : client.avgUploadSpeed) }}/s</span>
</td>
</tr>
</tbody>
</table>
</div>
<canvas ref="chart" :width="this.canvasWidth"></canvas>
</div>
<script src="vue.min.js"></script>
<script src="smoothie.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
rawData: {},
serverMap: {},
maxTimespan: 120,
firstTimestamp: undefined,
peak: false,
ports: false,
canvasWidth: 0,
smoothie: null,
graphColors: ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]
},
computed: {
servers () {
const servers = Object.values(this.serverMap)
servers.sort(this.compareObjectIps)
servers.forEach(server => {
server.offline = (this.rawData.timestamp - server.timestamp) > 6000
const clients = Object.values(server.clientMap)
if (this.peak) {
server.clients = clients.filter(client => client.peakUploadSpeed > 0)
server.clients.sort((a, b) => b.peakUploadSpeed - a.peakUploadSpeed)
} else {
server.clients = clients.filter(client => client.avgUploadSpeed > 0)
server.clients.sort((a, b) => b.avgUploadSpeed - a.avgUploadSpeed)
}
})
return servers
},
timespanStartTime () {
return Math.max(this.firstTimestamp, this.rawData.timestamp - this.maxTimespan * 1000)
},
timespan () {
return (this.rawData.timestamp - this.timespanStartTime) / 1000
}
},
watch: {
rawData () {
if (!this.firstTimestamp) this.firstTimestamp = this.rawData.timestamp
// Remove outdated values
for (let serverIp in this.serverMap) {
const server = this.serverMap[serverIp]
server.log = server.log.filter(x => x.timestamp > this.timespanStartTime)
if (server.log.length === 0) {
this.smoothie.removeTimeSeries(server.graphLine)
this.graphColors.unshift(server.graphColor)
Vue.delete(this.serverMap, serverIp)
} else {
for (let clientIp in server.clientMap) {
const client = server.clientMap[clientIp]
client.log = client.log.filter(x => x.timestamp > this.timespanStartTime)
if (client.log.length === 0) Vue.delete(server.clientMap, clientIp)
}
}
}
// Process new data
if (this.rawData.servers) {
this.rawData.servers.forEach(serverData => {
// Update server
var server = this.serverMap[serverData.address]
if (!server) {
server = {
address: serverData.address,
log: [],
clientMap: {},
graphColor: this.graphColors.shift(),
graphLine: new TimeSeries({ logarithmicScale: true })
}
Vue.set(this.serverMap, serverData.address, server)
this.smoothie.addTimeSeries(server.graphLine, { lineWidth: 2, strokeStyle: server.graphColor })
}
server.log.push(serverData)
this.calcSpeed(server, serverData)
server.updated = true
server.graphLine.append(server.timestamp, server.uploadSpeed)
// Update clients
serverData.clients.forEach(clientData => {
clientData.timestamp = server.timestamp
var client = server.clientMap[clientData.address]
if (!client) Vue.set(server.clientMap, clientData.address, { log: [] })
client = server.clientMap[clientData.address]
client.log.push(clientData)
this.calcSpeed(client, clientData)
client.updated = true
})
})
}
// Calculate speed of servers/clients not included in the latest data
for (let serverIp in this.serverMap) {
const server = this.serverMap[serverIp]
if (server.updated) server.updated = false
else this.calcSpeed(server, {})
for (let clientIp in server.clientMap) {
const client = server.clientMap[clientIp]
if (client.updated) client.updated = false
else this.calcSpeed(client, {})
}
}
}
},
methods: {
async updateData () {
try {
const response = await fetch('/data2.json')
this.rawData = await response.json()
} catch {}
setTimeout(this.updateData, 2000)
},
calcSpeed (obj, objData) {
// Need a minimum of two values to calculate the speed
if (obj.log.length <= 1) return
// Calculate current speeds
var a = obj.log[obj.log.length - 2]
const b = obj.log[obj.log.length - 1]
var time = (b.timestamp - a.timestamp) / 1000
objData.uploadSpeed = (b.bytesSent - a.bytesSent) / time
objData.downloadSpeed = (b.bytesReceived - a.bytesReceived) / time
// Calculate peak speeds
objData.peakUploadSpeed = Math.max(...obj.log.map(x => x.uploadSpeed || 0))
objData.peakDownloadSpeed = Math.max(...obj.log.map(x => x.downloadSpeed || 0))
// Calculate average speeds
a = obj.log[0]
objData.avgUploadSpeed = (b.bytesSent - a.bytesSent) / this.timespan
objData.avgDownloadSpeed = (b.bytesReceived - a.bytesReceived) / this.timespan
Object.assign(obj, objData)
},
formatBytes (bytes) {
if (bytes < 1024) return bytes.toFixed(2) + ' B'
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KiB'
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MiB'
else if (bytes < 1099511627776) return (bytes / 1073741824).toFixed(2) + ' GiB'
else return (bytes / 1099511627776).toFixed(2) + ' TiB'
},
formatSeconds (seconds) {
return ( Math.floor(seconds / ( 3600 * 24 ) ) + 'd '
+ Math.floor(seconds / 3600 ) % 24 + 'h '
+ Math.floor(seconds / 60 ) % 60 + 'min' )
},
calcBackgroundStyle (speed) {
const colors = ['#eee', '#cfc', '#6f6', '#70bf30', '#f00', '#f65']
const limits = [1048576, 10485760, 104857600, 1073741824, 10737418240]
for (var i = 0; i < 4; ++i) {
if (speed < limits[i]) break
}
const percent = Math.round(Math.max(0, Math.min(100, speed / limits[i] * 100)))
return { background: `linear-gradient(90deg, ${colors[i+1]} ${percent}%, ${colors[i]} ${percent}%)` }
},
compareObjectIps (obj1, obj2) {
const parts1 = obj1.address.split('.').map(str => parseInt(str))
const parts2 = obj2.address.split('.').map(str => parseInt(str))
var result = parts1[0] - parts2[0]
if (result === 0) result = parts1[1] - parts2[1]
if (result === 0) result = parts1[2] - parts2[2]
if (result === 0) result = parts1[3] - parts2[3]
return result
},
logScale (value) {
return Math.log(value/100000 + 1)
},
formatIpAddress (ip) {
return this.ports ? ip : ip.substring(0, ip.lastIndexOf(':'))
}
},
created () {
const urlParams = new URLSearchParams(window.location.search)
this.maxTimespan = parseInt(urlParams.get('timespan')) || 120
this.peak = urlParams.get('peak') === 'true' || urlParams.get('peak') === ''
this.ports = urlParams.get('ports') === 'true' || urlParams.get('ports') === ''
this.updateData()
},
mounted () {
this.canvasWidth = window.innerWidth
window.addEventListener('resize', () => {
this.canvasWidth = window.innerWidth
})
this.smoothie = new SmoothieChart({
'millisPerPixel': 300,
'grid': { 'millisPerLine': 30000 },
timestampFormatter: SmoothieChart.timeFormatter,
yMaxFormatter: this.formatBytes,
yMinFormatter: this.formatBytes,
valueTransformFunction: this.logScale,
minValue: 0,
maxValue: 262144000,
labels: { fontSize: 13 }
})
this.smoothie.streamTo(this.$refs.chart, 2000)
}
})
</script>
</body>
</html>