summaryrefslogblamecommitdiffstats
path: root/static/table.html
blob: 72475867981322f2af476c30911bb3fe6187364b (plain) (tree)
1
2
3
4
5
6



                      

                         










                                   
  
                     
                       
                  

                            
   
 
           
                            
                           


                        
                
   



                            






                       
                    


                                
                           
                       
                                  
   


               


          

       
 
                
                                  
                                                                                                                                                          
               

                                                                                                













                                                                                                                               
                                                                          
                                                                          
                                               
                                                                               
                                                                                                                         
                                                                                                     
                 


                
          
                                                           

        
                                  

                                   




                   
                
                  
                     
                              
                
                 

                   






                                                                                                                                                                                                                                             
                                                                           









                                                                               

                          



                                                                                            
     
    

                



















                                                                                     
                                 


                                                         
                        
                      
                                          






                                                                                                           
           

                                            
                               
                                                                       

                           


                                                             
                                                                                   
                                                         

                                              
                                 
            
          

       
                                                                           

                                               





                                                    
         
       
     


                         



                                                   

                                       
                              
                                                            
                                     
 



                                                   

                                                                        
 
                              

                                                                                     
 

                                 

                                                                                    

                                 
      





                                                                                     






                                                          
                                                                        



                                                                            
                                                                                     
                                                                                                           











                                                                      


                                                                    
     


                                                                 
                                                                 
                                                                                
                                                                                   
                     









                                                      

                                      





                                                  





         
<!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>