index.html 17 KB

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