index.html 17 KB

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