index.html 17 KB

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