| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- <!DOCTYPE html>
- <html lang="en" ng-app="syncthing" ng-controller="relayDataController">
- <head>
- <meta charset="utf-8"/>
- <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <meta name="description" content=""/>
- <meta name="author" content=""/>
- <title>Relay stats</title>
- <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
- <link rel="stylesheet" href="//use.fontawesome.com/releases/v5.0.13/css/all.css"/>
- <style>
- #map {
- height: 600px;
- }
- .ng-cloak {
- display: none;
- }
- table {
- font-size: 11px !important;
- width: 100%;
- border: 1px;
- }
- td {
- padding: 0px !important;
- }
- tfoot td {
- font-weight: bold;
- }
- </style>
- </head>
- <body class="ng-cloak">
- <div class="container">
- <h1>Relay Pool Data</h1>
- <div ng-if="relays === undefined" class="text-center">
- <img src="//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif" alt=""/>
- <p>Please wait while we gather data</p>
- </div>
- <div>
- <div ng-show="relays !== undefined" class="ng-hide">
- <p>
- Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total).
- </p>
- </div>
- <div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
- <p>The circle size represents how much bytes the relay transferred relative to other relays</p>
- </div>
- <div>
- <table class="table table-striped table-condensed table">
- <thead>
- <tr>
- <th rowspan="2">Address</td>
- <th rowspan="2">
- <a ng-click="sortType = 'stats.numActiveSessions'; sortReverse = !sortReverse">
- Sessions
- <span ng-show="sortType == 'stats.numActiveSessions' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.numActiveSessions' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th rowspan="2">
- <a ng-click="sortType = 'stats.numConnections'; sortReverse = !sortReverse">
- Connections
- <span ng-show="sortType == 'stats.numConnections' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.numConnections' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th rowspan="2">
- <a ng-click="sortType = 'stats.bytesProxied'; sortReverse = !sortReverse">
- Data relayed
- <span ng-show="sortType == 'stats.bytesProxied' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.bytesProxied' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th colspan="6" class="text-center">Transfer rate in the last period</th>
- <th rowspan="2">
- <a ng-click="sortType = 'stats.uptimeSeconds'; sortReverse = !sortReverse">
- Uptime hours
- <span ng-show="sortType == 'stats.uptimeSeconds' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'status.uptimeSeconds' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th rowspan="2">
- <a ng-click="sortType = 'stats.options[\'provided-by\'] || \'\''; sortReverse = !sortReverse">
- Provided by
- <span ng-show="sortType == 'stats.options[\'provided-by\'] || \'\'' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.options[\'provided-by\'] || \'\'' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- </tr>
- <tr>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[0]'; sortReverse = !sortReverse">
- 10s
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[0]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[0]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[1]'; sortReverse = !sortReverse">
- 1m
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[1]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[1]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[2]'; sortReverse = !sortReverse">
- 5m
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[2]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[2]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[3]'; sortReverse = !sortReverse">
- 15m
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[3]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[3]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[4]'; sortReverse = !sortReverse">
- 30m
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[4]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[4]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- <th>
- <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[5]'; sortReverse = !sortReverse">
- 60m
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[5]' && !sortReverse" class="fas fa-caret-down"></span>
- <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[5]' && sortReverse" class="fas fa-caret-up"></span>
- </a>
- </th>
- </tr>
- </thead>
- <tbody>
- <tr ng-repeat="relay in relays | orderBy:sortType:sortReverse:sortCompare" ng-mouseover="relay.showMarker()" ng-mouseleave="relay.hideMarker()">
- <td>{{ relay.address }}</td>
- <td ng-if="!relay.stats" colspan="11"></td>
- <td ng-if-start="relay.stats">{{ relay.stats.numActiveSessions }}</td>
- <td>{{ relay.stats.numConnections }}</td>
- <td>{{ relay.stats.bytesProxied | bytes }}</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
- <td>{{ relay.stats.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
- <td ng-if="relay.stats.uptimeSeconds != undefined">{{ relay.stats.uptimeSeconds/60/60 | number:0 }}</td>
- <td ng-if="relay.stats.uptimeSeconds == undefined"></td>
- <td title="{{ relay.stats.options['provided-by'] || '' }}" ng-if-end>
- {{ relay.stats.options['provided-by'] || '' | limitTo:50 }}
- <span ng-if="(relay.stats.options['provided-by'] || '').length > 50">…
- </td>
- </tr>
- </tbody>
- <tfoot>
- <tr>
- <td>Totals</td>
- <td>{{ totals.numActiveSessions }}</td>
- <td>{{ totals.numConnections }}</td>
- <td>{{ totals.bytesProxied | bytes }}</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
- <td>{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
- <td>{{ totals.uptimeSeconds/60/60 | number:0 }} hours</td>
- <td>{{ relays.length }} relays</td>
- </tr>
- </tfoor>
- </table>
- </div>
- <hr>
- <p>
- This product includes GeoLite2 data created by MaxMind, available from
- <a href="http://www.maxmind.com">http://www.maxmind.com</a>.
- </p>
- </div>
- <script type="text/javascript" src="//code.jquery.com/jquery-2.1.4.min.js"></script>
- <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
- <script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
- <script type="text/javascript" src="//maps.googleapis.com/maps/api/js"></script>
- </body>
- <script>
- angular.module('syncthing', [
- ])
- .config(function($httpProvider) {
- $httpProvider.defaults.timeout = 5000;
- })
- .filter('bytes', function() {
- return function(bytes, precision) {
- if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
- if (typeof precision === 'undefined') precision = 1;
- var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
- number = Math.floor(Math.log(bytes) / Math.log(1024));
- var value = (bytes / Math.pow(1000, Math.floor(number)));
- if (!isFinite(value)) {
- value = 0;
- precision = 0;
- }
- if (!isFinite(number)) {
- units = 'bytes';
- } else {
- units = units[number];
- }
- return value.toFixed(precision) + ' ' + units;
- }
- })
- .controller('relayDataController', ['$scope', '$rootScope', '$http', '$q', '$compile', '$timeout', function($scope, $rootScope, $http, $q, $compile, $timeout) {
- $scope.totals = {
- bytesProxied: 0,
- goMaxProcs: 0,
- kbps10s1m5m15m30m60m: [0, 0, 0, 0, 0, 0],
- numActiveSessions: 0,
- numConnections: 0,
- numPendingSessionKeys: 0,
- numProxies: 0,
- uptimeSeconds: 0,
- };
- $scope.map = new google.maps.Map(document.getElementById('map'), {
- zoom: 1,
- mapTypeId: google.maps.MapTypeId.ROADMAP
- });
- $scope.mapBounds = new google.maps.LatLngBounds();
- $scope.tooltipTemplate = $('#infoTemplate').html();
- $scope.usedLocations = {};
- $scope.sortType = 'stats.numActiveSessions';
- $scope.sortReverse = true;
- $scope.sortCompare = function(a, b) {
- if (a.value == b.value) {
- return 0;
- }
- if (a.type == "undefined" || a.type == "null") {
- return -1;
- }
- if (b.type == "undefined" || b.type == "null") {
- return 1;
- }
- return a.value > b.value ? 1 : -1;
- }
- $http.get("/endpoint").then(function(response) {
- $scope.relays = response.data.relays;
- angular.forEach($scope.relays, function(relay) {
- relay.uri = constructURI(relay.url);
- relay.address = relay.url.split('/')[2];
- addMarkerToMap(relay);
- if (relay.stats) {
- angular.forEach($scope.totals, function(value, key) {
- if (typeof $scope.totals[key] == 'number') {
- $scope.totals[key] += relay.stats[key];
- } else if (typeof $scope.totals[key] == 'object' && $scope.totals[key] instanceof Array) {
- angular.forEach($scope.totals[key], function(value, index) {
- $scope.totals[key][index] += relay.stats[key][index];
- });
- }
- });
- }
- });
- // After the totals were calculated, add circles.
- angular.forEach($scope.relays, function(relay) {
- if (relay.stats) {
- addCircleToMap(relay);
- }
- });
- $scope.map.fitBounds($scope.mapBounds);
- if ($scope.relays.length == 1) {
- $scope.map.setZoom(13);
- }
- });
- function addMarkerToMap(relay) {
- var loc = relay.location.latitude + "," + relay.location.longitude;
- // Deal with overlapping markers
- while (loc in $scope.usedLocations) {
- var locParts = loc.split(',');
- locParts = [parseFloat(locParts[0]), parseFloat(locParts[1])];
- locParts[Math.round(Math.random())] += 0.5 * (Math.random() >= 0.5 ? 1 : -1);
- loc = locParts.join(',');
- }
- $scope.usedLocations[loc] = true;
- var locParts = loc.split(',');
- relay.marker = new google.maps.Marker({
- map: $scope.map,
- position: new google.maps.LatLng(locParts[0], locParts[1]),
- title: relay.url,
- });
- var scope = $rootScope.$new(true);
- scope.relay = relay;
- relay.marker.info = new google.maps.InfoWindow({
- content: $compile($scope.tooltipTemplate)(scope)[0],
- });
- relay.showMarker = function() {
- relay.marker.info.open($scope.map, relay.marker);
- }
- relay.hideMarker = function() {
- relay.marker.info.close();
- }
- relay.marker.addListener('mouseover', relay.showMarker);
- relay.marker.addListener('mouseout', relay.hideMarker);
- $scope.mapBounds.extend(relay.marker.position);
- }
- function addCircleToMap(relay) {
- relay.marker.circle = new google.maps.Circle({
- strokeColor: '#FF0000',
- strokeOpacity: 0.8,
- strokeWeight: 2,
- fillColor: '#FF0000',
- fillOpacity: 0.35,
- map: $scope.map,
- center: relay.marker.position,
- radius: ((relay.stats.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000
- });
- }
- function constructURI(url) {
- var uri = document.createElement('a');
- // HAX, otherwise doesn't work
- uri.href = url.replace('relay://', 'http://');
- // Convert query string to object
- uri.args = {};
- angular.forEach(uri.search.replace(/^\?/, '').split('&'), function(query) {
- var split = query.split('=');
- uri.args[split[0]] = split[1];
- });
- return uri;
- }
- }]);
- </script>
- <script type="text/template" id="infoTemplate">
- <div>
- <p><b>{{ relay.uri.hostname }}</b> <span ng-if="relay.stats.options['provided-by']">provided by <u>{{ relay.stats.options['provided-by'] }}</u></span></p>
- <div ng-if="relay.stats">
- <span ng-if="relay.stats.startTime">Start time: {{ relay.stats.startTime | date:"medium" }}</br></span>
- <span ng-if="relay.stats.bytesProxied != undefined">Proxied: {{ relay.stats.bytesProxied | bytes }}</br></span>
- <span ng-if="relay.stats.numActiveSessions != undefined">Sessions: {{ relay.stats.numActiveSessions }}</br></span>
- <span ng-if="relay.stats.numConnections != undefined">Clients: {{ relay.stats.numConnections }}</br></span>
- <span ng-if="relay.stats.options.pools">Pools: {{ relay.stats.options.pools.join(', ') }}</br></span>
- <span ng-if="relay.stats.options['global-rate'] != undefined">
- <span ng-if="relay.stats.options['global-rate'] > 0">Global rate limit: {{ relay.stats.options['global-rate'] | bytes }}/s</span>
- <span ng-if="relay.stats.options['global-rate'] == 0">Global rate limit: unlimited</span>
- <br/>
- </span>
- <span ng-if="relay.stats.options['per-session-rate'] != undefined">
- <span ng-if="relay.stats.options['per-session-rate'] > 0">Session rate limit: {{ relay.stats.options['per-session-rate'] | bytes }}/s</span>
- <span ng-if="relay.stats.options['per-session-rate'] == 0">Session rate limit: unlimited</span>
- <br/>
- </span>
- </div>
- <div ng-if="!relay.stats">
- Data unavailable.
- <div>
- </div>
- </script>
- </html>
|