index.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <!DOCTYPE html>
  2. <html lang="en" ng-app="syncthing" ng-controller="relayDataController">
  3. <head>
  4. <meta charset="utf-8"/>
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  7. <meta name="description" content=""/>
  8. <meta name="author" content=""/>
  9. <title>Relay stats</title>
  10. <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
  11. <link rel="stylesheet" href="//use.fontawesome.com/releases/v5.0.13/css/all.css"/>
  12. <style>
  13. #map {
  14. height: 600px;
  15. }
  16. .ng-cloak {
  17. display: none;
  18. }
  19. table {
  20. font-size: 11px !important;
  21. width: 100%;
  22. border: 1px;
  23. }
  24. td {
  25. padding: 0px !important;
  26. }
  27. tfoot td {
  28. font-weight: bold;
  29. }
  30. </style>
  31. </head>
  32. <body class="ng-cloak">
  33. <div class="container">
  34. <h1>Relay Pool Data</h1>
  35. <div ng-if="relays === undefined" class="text-center">
  36. <img src="//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif" alt=""/>
  37. <p>Please wait while we gather data</p>
  38. </div>
  39. <div>
  40. <div ng-show="relays !== undefined" class="ng-hide">
  41. <p>
  42. Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total).
  43. </p>
  44. </div>
  45. <div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
  46. <p>The circle size represents how much bytes the relay transferred relative to other relays</p>
  47. </div>
  48. <div>
  49. <table class="table table-striped table-condensed table">
  50. <thead>
  51. <tr>
  52. <th rowspan="2">Address</td>
  53. <th rowspan="2">
  54. <a ng-click="sortType = 'stats.numActiveSessions'; sortReverse = !sortReverse">
  55. Sessions
  56. <span ng-show="sortType == 'stats.numActiveSessions' && !sortReverse" class="fas fa-caret-down"></span>
  57. <span ng-show="sortType == 'stats.numActiveSessions' && sortReverse" class="fas fa-caret-up"></span>
  58. </a>
  59. </th>
  60. <th rowspan="2">
  61. <a ng-click="sortType = 'stats.numConnections'; sortReverse = !sortReverse">
  62. Connections
  63. <span ng-show="sortType == 'stats.numConnections' && !sortReverse" class="fas fa-caret-down"></span>
  64. <span ng-show="sortType == 'stats.numConnections' && sortReverse" class="fas fa-caret-up"></span>
  65. </a>
  66. </th>
  67. <th rowspan="2">
  68. <a ng-click="sortType = 'stats.bytesProxied'; sortReverse = !sortReverse">
  69. Data relayed
  70. <span ng-show="sortType == 'stats.bytesProxied' && !sortReverse" class="fas fa-caret-down"></span>
  71. <span ng-show="sortType == 'stats.bytesProxied' && sortReverse" class="fas fa-caret-up"></span>
  72. </a>
  73. </th>
  74. <th colspan="6" class="text-center">Transfer rate in the last period</th>
  75. <th rowspan="2">
  76. <a ng-click="sortType = 'stats.uptimeSeconds'; sortReverse = !sortReverse">
  77. Uptime hours
  78. <span ng-show="sortType == 'stats.uptimeSeconds' && !sortReverse" class="fas fa-caret-down"></span>
  79. <span ng-show="sortType == 'status.uptimeSeconds' && sortReverse" class="fas fa-caret-up"></span>
  80. </a>
  81. </th>
  82. <th rowspan="2">
  83. <a ng-click="sortType = 'stats.options[\'provided-by\'] || \'\''; sortReverse = !sortReverse">
  84. Provided by
  85. <span ng-show="sortType == 'stats.options[\'provided-by\'] || \'\'' && !sortReverse" class="fas fa-caret-down"></span>
  86. <span ng-show="sortType == 'stats.options[\'provided-by\'] || \'\'' && sortReverse" class="fas fa-caret-up"></span>
  87. </a>
  88. </th>
  89. </tr>
  90. <tr>
  91. <th>
  92. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[0]'; sortReverse = !sortReverse">
  93. 10s
  94. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[0]' && !sortReverse" class="fas fa-caret-down"></span>
  95. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[0]' && sortReverse" class="fas fa-caret-up"></span>
  96. </a>
  97. </th>
  98. <th>
  99. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[1]'; sortReverse = !sortReverse">
  100. 1m
  101. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[1]' && !sortReverse" class="fas fa-caret-down"></span>
  102. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[1]' && sortReverse" class="fas fa-caret-up"></span>
  103. </a>
  104. </th>
  105. <th>
  106. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[2]'; sortReverse = !sortReverse">
  107. 5m
  108. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[2]' && !sortReverse" class="fas fa-caret-down"></span>
  109. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[2]' && sortReverse" class="fas fa-caret-up"></span>
  110. </a>
  111. </th>
  112. <th>
  113. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[3]'; sortReverse = !sortReverse">
  114. 15m
  115. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[3]' && !sortReverse" class="fas fa-caret-down"></span>
  116. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[3]' && sortReverse" class="fas fa-caret-up"></span>
  117. </a>
  118. </th>
  119. <th>
  120. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[4]'; sortReverse = !sortReverse">
  121. 30m
  122. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[4]' && !sortReverse" class="fas fa-caret-down"></span>
  123. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[4]' && sortReverse" class="fas fa-caret-up"></span>
  124. </a>
  125. </th>
  126. <th>
  127. <a ng-click="sortType = 'stats.kbps10s1m5m15m30m60m[5]'; sortReverse = !sortReverse">
  128. 60m
  129. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[5]' && !sortReverse" class="fas fa-caret-down"></span>
  130. <span ng-show="sortType == 'stats.kbps10s1m5m15m30m60m[5]' && sortReverse" class="fas fa-caret-up"></span>
  131. </a>
  132. </th>
  133. </tr>
  134. </thead>
  135. <tbody>
  136. <tr ng-repeat="relay in relays | orderBy:sortType:sortReverse:sortCompare" ng-mouseover="relay.showMarker()" ng-mouseleave="relay.hideMarker()">
  137. <td>{{ relay.address }}</td>
  138. <td ng-if="!relay.stats" colspan="11"></td>
  139. <td ng-if-start="relay.stats">{{ relay.stats.numActiveSessions }}</td>
  140. <td>{{ relay.stats.numConnections }}</td>
  141. <td>{{ relay.stats.bytesProxied | bytes }}</td>
  142. <td>{{ relay.stats.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
  143. <td>{{ relay.stats.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
  144. <td>{{ relay.stats.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
  145. <td>{{ relay.stats.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
  146. <td>{{ relay.stats.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
  147. <td>{{ relay.stats.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
  148. <td ng-if="relay.stats.uptimeSeconds != undefined">{{ relay.stats.uptimeSeconds/60/60 | number:0 }}</td>
  149. <td ng-if="relay.stats.uptimeSeconds == undefined"></td>
  150. <td title="{{ relay.stats.options['provided-by'] || '' }}" ng-if-end>
  151. {{ relay.stats.options['provided-by'] || '' | limitTo:50 }}
  152. <span ng-if="(relay.stats.options['provided-by'] || '').length > 50">&hellip;
  153. </td>
  154. </tr>
  155. </tbody>
  156. <tfoot>
  157. <tr>
  158. <td>Totals</td>
  159. <td>{{ totals.numActiveSessions }}</td>
  160. <td>{{ totals.numConnections }}</td>
  161. <td>{{ totals.bytesProxied | bytes }}</td>
  162. <td>{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
  163. <td>{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
  164. <td>{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
  165. <td>{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
  166. <td>{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
  167. <td>{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
  168. <td>{{ totals.uptimeSeconds/60/60 | number:0 }} hours</td>
  169. <td>{{ relays.length }} relays</td>
  170. </tr>
  171. </tfoor>
  172. </table>
  173. </div>
  174. <hr>
  175. <p>
  176. This product includes GeoLite2 data created by MaxMind, available from
  177. <a href="http://www.maxmind.com">http://www.maxmind.com</a>.
  178. </p>
  179. </div>
  180. <script type="text/javascript" src="//code.jquery.com/jquery-2.1.4.min.js"></script>
  181. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
  182. <script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  183. <script type="text/javascript" src="//maps.googleapis.com/maps/api/js"></script>
  184. </body>
  185. <script>
  186. angular.module('syncthing', [
  187. ])
  188. .config(function($httpProvider) {
  189. $httpProvider.defaults.timeout = 5000;
  190. })
  191. .filter('bytes', function() {
  192. return function(bytes, precision) {
  193. if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
  194. if (typeof precision === 'undefined') precision = 1;
  195. var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
  196. number = Math.floor(Math.log(bytes) / Math.log(1024));
  197. var value = (bytes / Math.pow(1000, Math.floor(number)));
  198. if (!isFinite(value)) {
  199. value = 0;
  200. precision = 0;
  201. }
  202. if (!isFinite(number)) {
  203. units = 'bytes';
  204. } else {
  205. units = units[number];
  206. }
  207. return value.toFixed(precision) + ' ' + units;
  208. }
  209. })
  210. .controller('relayDataController', ['$scope', '$rootScope', '$http', '$q', '$compile', '$timeout', function($scope, $rootScope, $http, $q, $compile, $timeout) {
  211. $scope.totals = {
  212. bytesProxied: 0,
  213. goMaxProcs: 0,
  214. kbps10s1m5m15m30m60m: [0, 0, 0, 0, 0, 0],
  215. numActiveSessions: 0,
  216. numConnections: 0,
  217. numPendingSessionKeys: 0,
  218. numProxies: 0,
  219. uptimeSeconds: 0,
  220. };
  221. $scope.map = new google.maps.Map(document.getElementById('map'), {
  222. zoom: 1,
  223. mapTypeId: google.maps.MapTypeId.ROADMAP
  224. });
  225. $scope.mapBounds = new google.maps.LatLngBounds();
  226. $scope.tooltipTemplate = $('#infoTemplate').html();
  227. $scope.usedLocations = {};
  228. $scope.sortType = 'stats.numActiveSessions';
  229. $scope.sortReverse = true;
  230. $scope.sortCompare = function(a, b) {
  231. if (a.value == b.value) {
  232. return 0;
  233. }
  234. if (a.type == "undefined" || a.type == "null") {
  235. return -1;
  236. }
  237. if (b.type == "undefined" || b.type == "null") {
  238. return 1;
  239. }
  240. return a.value > b.value ? 1 : -1;
  241. }
  242. $http.get("/endpoint").then(function(response) {
  243. $scope.relays = response.data.relays;
  244. angular.forEach($scope.relays, function(relay) {
  245. relay.uri = constructURI(relay.url);
  246. relay.address = relay.url.split('/')[2];
  247. addMarkerToMap(relay);
  248. if (relay.stats) {
  249. angular.forEach($scope.totals, function(value, key) {
  250. if (typeof $scope.totals[key] == 'number') {
  251. $scope.totals[key] += relay.stats[key];
  252. } else if (typeof $scope.totals[key] == 'object' && $scope.totals[key] instanceof Array) {
  253. angular.forEach($scope.totals[key], function(value, index) {
  254. $scope.totals[key][index] += relay.stats[key][index];
  255. });
  256. }
  257. });
  258. }
  259. });
  260. // After the totals were calculated, add circles.
  261. angular.forEach($scope.relays, function(relay) {
  262. if (relay.stats) {
  263. addCircleToMap(relay);
  264. }
  265. });
  266. $scope.map.fitBounds($scope.mapBounds);
  267. if ($scope.relays.length == 1) {
  268. $scope.map.setZoom(13);
  269. }
  270. });
  271. function addMarkerToMap(relay) {
  272. var loc = relay.location.latitude + "," + relay.location.longitude;
  273. // Deal with overlapping markers
  274. while (loc in $scope.usedLocations) {
  275. var locParts = loc.split(',');
  276. locParts = [parseFloat(locParts[0]), parseFloat(locParts[1])];
  277. locParts[Math.round(Math.random())] += 0.5 * (Math.random() >= 0.5 ? 1 : -1);
  278. loc = locParts.join(',');
  279. }
  280. $scope.usedLocations[loc] = true;
  281. var locParts = loc.split(',');
  282. relay.marker = new google.maps.Marker({
  283. map: $scope.map,
  284. position: new google.maps.LatLng(locParts[0], locParts[1]),
  285. title: relay.url,
  286. });
  287. var scope = $rootScope.$new(true);
  288. scope.relay = relay;
  289. relay.marker.info = new google.maps.InfoWindow({
  290. content: $compile($scope.tooltipTemplate)(scope)[0],
  291. });
  292. relay.showMarker = function() {
  293. relay.marker.info.open($scope.map, relay.marker);
  294. }
  295. relay.hideMarker = function() {
  296. relay.marker.info.close();
  297. }
  298. relay.marker.addListener('mouseover', relay.showMarker);
  299. relay.marker.addListener('mouseout', relay.hideMarker);
  300. $scope.mapBounds.extend(relay.marker.position);
  301. }
  302. function addCircleToMap(relay) {
  303. relay.marker.circle = new google.maps.Circle({
  304. strokeColor: '#FF0000',
  305. strokeOpacity: 0.8,
  306. strokeWeight: 2,
  307. fillColor: '#FF0000',
  308. fillOpacity: 0.35,
  309. map: $scope.map,
  310. center: relay.marker.position,
  311. radius: ((relay.stats.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000
  312. });
  313. }
  314. function constructURI(url) {
  315. var uri = document.createElement('a');
  316. // HAX, otherwise doesn't work
  317. uri.href = url.replace('relay://', 'http://');
  318. // Convert query string to object
  319. uri.args = {};
  320. angular.forEach(uri.search.replace(/^\?/, '').split('&'), function(query) {
  321. var split = query.split('=');
  322. uri.args[split[0]] = split[1];
  323. });
  324. return uri;
  325. }
  326. }]);
  327. </script>
  328. <script type="text/template" id="infoTemplate">
  329. <div>
  330. <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>
  331. <div ng-if="relay.stats">
  332. <span ng-if="relay.stats.startTime">Start time: {{ relay.stats.startTime | date:"medium" }}</br></span>
  333. <span ng-if="relay.stats.bytesProxied != undefined">Proxied: {{ relay.stats.bytesProxied | bytes }}</br></span>
  334. <span ng-if="relay.stats.numActiveSessions != undefined">Sessions: {{ relay.stats.numActiveSessions }}</br></span>
  335. <span ng-if="relay.stats.numConnections != undefined">Clients: {{ relay.stats.numConnections }}</br></span>
  336. <span ng-if="relay.stats.options.pools">Pools: {{ relay.stats.options.pools.join(', ') }}</br></span>
  337. <span ng-if="relay.stats.options['global-rate'] != undefined">
  338. <span ng-if="relay.stats.options['global-rate'] > 0">Global rate limit: {{ relay.stats.options['global-rate'] | bytes }}/s</span>
  339. <span ng-if="relay.stats.options['global-rate'] == 0">Global rate limit: unlimited</span>
  340. <br/>
  341. </span>
  342. <span ng-if="relay.stats.options['per-session-rate'] != undefined">
  343. <span ng-if="relay.stats.options['per-session-rate'] > 0">Session rate limit: {{ relay.stats.options['per-session-rate'] | bytes }}/s</span>
  344. <span ng-if="relay.stats.options['per-session-rate'] == 0">Session rate limit: unlimited</span>
  345. <br/>
  346. </span>
  347. </div>
  348. <div ng-if="!relay.stats">
  349. Data unavailable.
  350. <div>
  351. </div>
  352. </script>
  353. </html>