syncthingController.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323
  1. angular.module('syncthing.core')
  2. .config(function($locationProvider) {
  3. $locationProvider.html5Mode(true).hashPrefix('!');
  4. })
  5. .controller('SyncthingController', function ($scope, $http, $location, LocaleService) {
  6. 'use strict';
  7. // private/helper definitions
  8. var prevDate = 0;
  9. var navigatingAway = false;
  10. var online = false;
  11. var restarting = false;
  12. function initController() {
  13. LocaleService.autoConfigLocale();
  14. setInterval($scope.refresh, 10000);
  15. }
  16. // pubic/scope definitions
  17. $scope.completion = {};
  18. $scope.config = {};
  19. $scope.configInSync = true;
  20. $scope.connections = {};
  21. $scope.errors = [];
  22. $scope.model = {};
  23. $scope.myID = '';
  24. $scope.devices = [];
  25. $scope.deviceRejections = {};
  26. $scope.folderRejections = {};
  27. $scope.protocolChanged = false;
  28. $scope.reportData = {};
  29. $scope.reportPreview = false;
  30. $scope.folders = {};
  31. $scope.seenError = '';
  32. $scope.upgradeInfo = null;
  33. $scope.deviceStats = {};
  34. $scope.folderStats = {};
  35. $scope.progress = {};
  36. $scope.version = {};
  37. $scope.needed = [];
  38. $scope.neededTotal = 0;
  39. $scope.neededCurrentPage = 1;
  40. $scope.neededPageSize = 10;
  41. $(window).bind('beforeunload', function () {
  42. navigatingAway = true;
  43. });
  44. $scope.$on("$locationChangeSuccess", function () {
  45. LocaleService.useLocale($location.search().lang);
  46. });
  47. $scope.needActions = {
  48. 'rm': 'Del',
  49. 'rmdir': 'Del (dir)',
  50. 'sync': 'Sync',
  51. 'touch': 'Update'
  52. };
  53. $scope.needIcons = {
  54. 'rm': 'remove',
  55. 'rmdir': 'remove',
  56. 'sync': 'download',
  57. 'touch': 'asterisk'
  58. };
  59. $scope.$on('UIOnline', function (event, arg) {
  60. if (online && !restarting) {
  61. return;
  62. }
  63. console.log('UIOnline');
  64. refreshSystem();
  65. refreshConfig();
  66. refreshConnectionStats();
  67. refreshDeviceStats();
  68. refreshFolderStats();
  69. $http.get(urlbase + '/system/version').success(function (data) {
  70. $scope.version = data;
  71. }).error($scope.emitHTTPError);
  72. $http.get(urlbase + '/svc/report').success(function (data) {
  73. $scope.reportData = data;
  74. }).error($scope.emitHTTPError);
  75. $http.get(urlbase + '/system/upgrade').success(function (data) {
  76. $scope.upgradeInfo = data;
  77. }).error(function () {
  78. $scope.upgradeInfo = null;
  79. });
  80. online = true;
  81. restarting = false;
  82. $('#networkError').modal('hide');
  83. $('#restarting').modal('hide');
  84. $('#shutdown').modal('hide');
  85. });
  86. $scope.$on('UIOffline', function (event, arg) {
  87. if (navigatingAway || !online) {
  88. return;
  89. }
  90. console.log('UIOffline');
  91. online = false;
  92. if (!restarting) {
  93. $('#networkError').modal();
  94. }
  95. });
  96. $scope.$on('HTTPError', function (event, arg) {
  97. // Emitted when a HTTP call fails. We use the status code to try
  98. // to figure out what's wrong.
  99. if (navigatingAway || !online) {
  100. return;
  101. }
  102. console.log('HTTPError', arg);
  103. online = false;
  104. if (!restarting) {
  105. if (arg.status === 0) {
  106. // A network error, not an HTTP error
  107. $scope.$emit('UIOffline');
  108. } else if (arg.status >= 400 && arg.status <= 599) {
  109. // A genuine HTTP error
  110. $('#networkError').modal('hide');
  111. $('#restarting').modal('hide');
  112. $('#shutdown').modal('hide');
  113. $('#httpError').modal();
  114. }
  115. }
  116. });
  117. $scope.$on('StateChanged', function (event, arg) {
  118. var data = arg.data;
  119. if ($scope.model[data.folder]) {
  120. $scope.model[data.folder].state = data.to;
  121. }
  122. });
  123. $scope.$on('LocalIndexUpdated', function (event, arg) {
  124. var data = arg.data;
  125. refreshFolderStats();
  126. });
  127. $scope.$on('RemoteIndexUpdated', function (event, arg) {
  128. // Nothing
  129. });
  130. $scope.$on('DeviceDisconnected', function (event, arg) {
  131. delete $scope.connections[arg.data.id];
  132. refreshDeviceStats();
  133. });
  134. $scope.$on('DeviceConnected', function (event, arg) {
  135. if (!$scope.connections[arg.data.id]) {
  136. $scope.connections[arg.data.id] = {
  137. inbps: 0,
  138. outbps: 0,
  139. inBytesTotal: 0,
  140. outBytesTotal: 0,
  141. address: arg.data.addr
  142. };
  143. $scope.completion[arg.data.id] = {
  144. _total: 100
  145. };
  146. }
  147. });
  148. $scope.$on('ConfigLoaded', function (event) {
  149. if ($scope.config.options.urAccepted === 0) {
  150. // If usage reporting has been neither accepted nor declined,
  151. // we want to ask the user to make a choice. But we don't want
  152. // to bug them during initial setup, so we set a cookie with
  153. // the time of the first visit. When that cookie is present
  154. // and the time is more than four hours ago, we ask the
  155. // question.
  156. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  157. if (!firstVisit) {
  158. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  159. } else {
  160. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  161. $('#ur').modal();
  162. }
  163. }
  164. }
  165. });
  166. $scope.$on('DeviceRejected', function (event, arg) {
  167. $scope.deviceRejections[arg.data.device] = arg;
  168. });
  169. $scope.$on('FolderRejected', function (event, arg) {
  170. $scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
  171. });
  172. $scope.$on('ConfigSaved', function (event, arg) {
  173. updateLocalConfig(arg.data);
  174. $http.get(urlbase + '/system/config/insync').success(function (data) {
  175. $scope.configInSync = data.configInSync;
  176. }).error($scope.emitHTTPError);
  177. });
  178. $scope.$on('DownloadProgress', function (event, arg) {
  179. var stats = arg.data;
  180. var progress = {};
  181. for (var folder in stats) {
  182. progress[folder] = {};
  183. for (var file in stats[folder]) {
  184. var s = stats[folder][file];
  185. var reused = 100 * s.reused / s.total;
  186. var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
  187. var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
  188. var pulled = 100 * s.pulled / s.total;
  189. var pulling = 100 * s.pulling / s.total;
  190. // We try to round up pulling to at least a percent so that it would be at least a bit visible.
  191. if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
  192. pulling = 1;
  193. }
  194. progress[folder][file] = {
  195. reused: reused,
  196. copiedFromOrigin: copiedFromOrigin,
  197. copiedFromElsewhere: copiedFromElsewhere,
  198. pulled: pulled,
  199. pulling: pulling,
  200. bytesTotal: s.bytesTotal,
  201. bytesDone: s.bytesDone,
  202. };
  203. }
  204. }
  205. for (var folder in $scope.progress) {
  206. if (!(folder in progress)) {
  207. if ($scope.neededFolder == folder) {
  208. refreshNeed(folder);
  209. }
  210. } else if ($scope.neededFolder == folder) {
  211. for (file in $scope.progress[folder]) {
  212. if (!(file in progress[folder])) {
  213. refreshNeed(folder);
  214. break;
  215. }
  216. }
  217. }
  218. }
  219. $scope.progress = progress;
  220. console.log("DownloadProgress", $scope.progress);
  221. });
  222. $scope.$on('FolderSummary', function (event, arg) {
  223. var data = arg.data;
  224. $scope.model[data.folder] = data.summary;
  225. });
  226. $scope.$on('FolderCompletion', function (event, arg) {
  227. var data = arg.data;
  228. if (!$scope.completion[data.device]) {
  229. $scope.completion[data.device] = {};
  230. }
  231. $scope.completion[data.device][data.folder] = data.completion;
  232. var tot = 0,
  233. cnt = 0;
  234. for (var cmp in $scope.completion[data.device]) {
  235. if (cmp === "_total") {
  236. continue;
  237. }
  238. tot += $scope.completion[data.device][cmp];
  239. cnt += 1;
  240. }
  241. $scope.completion[data.device]._total = tot / cnt;
  242. });
  243. $scope.emitHTTPError = function (data, status, headers, config) {
  244. $scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
  245. };
  246. var debouncedFuncs = {};
  247. function refreshFolder(folder) {
  248. var key = "refreshFolder" + folder;
  249. if (!debouncedFuncs[key]) {
  250. debouncedFuncs[key] = debounce(function () {
  251. $http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
  252. $scope.model[folder] = data;
  253. console.log("refreshFolder", folder, data);
  254. }).error($scope.emitHTTPError);
  255. }, 1000, true);
  256. }
  257. debouncedFuncs[key]();
  258. }
  259. function updateLocalConfig(config) {
  260. var hasConfig = !isEmptyObject($scope.config);
  261. $scope.config = config;
  262. $scope.config.options.listenAddressStr = $scope.config.options.listenAddress.join(', ');
  263. $scope.config.options.globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
  264. $scope.devices = $scope.config.devices;
  265. $scope.devices.forEach(function (deviceCfg) {
  266. $scope.completion[deviceCfg.deviceID] = {
  267. _total: 100
  268. };
  269. });
  270. $scope.devices.sort(deviceCompare);
  271. $scope.folders = folderMap($scope.config.folders);
  272. Object.keys($scope.folders).forEach(function (folder) {
  273. refreshFolder(folder);
  274. $scope.folders[folder].devices.forEach(function (deviceCfg) {
  275. refreshCompletion(deviceCfg.deviceID, folder);
  276. });
  277. });
  278. if (!hasConfig) {
  279. $scope.$emit('ConfigLoaded');
  280. }
  281. }
  282. function refreshSystem() {
  283. $http.get(urlbase + '/system/status').success(function (data) {
  284. $scope.myID = data.myID;
  285. $scope.system = data;
  286. $scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
  287. var failed = [];
  288. for (var server in data.extAnnounceOK) {
  289. if (!data.extAnnounceOK[server]) {
  290. failed.push(server);
  291. }
  292. }
  293. $scope.announceServersFailed = failed;
  294. console.log("refreshSystem", data);
  295. }).error($scope.emitHTTPError);
  296. }
  297. function refreshCompletion(device, folder) {
  298. if (device === $scope.myID) {
  299. return;
  300. }
  301. $http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
  302. if (!$scope.completion[device]) {
  303. $scope.completion[device] = {};
  304. }
  305. $scope.completion[device][folder] = data.completion;
  306. var tot = 0,
  307. cnt = 0;
  308. for (var cmp in $scope.completion[device]) {
  309. if (cmp === "_total") {
  310. continue;
  311. }
  312. tot += $scope.completion[device][cmp];
  313. cnt += 1;
  314. }
  315. $scope.completion[device]._total = tot / cnt;
  316. console.log("refreshCompletion", device, folder, $scope.completion[device]);
  317. }).error($scope.emitHTTPError);
  318. }
  319. function refreshConnectionStats() {
  320. $http.get(urlbase + '/system/connections').success(function (data) {
  321. var now = Date.now(),
  322. td = (now - prevDate) / 1000,
  323. id;
  324. prevDate = now;
  325. try {
  326. data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
  327. data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
  328. } catch (e) {
  329. data.total.inbps = 0;
  330. data.total.outbps = 0;
  331. }
  332. $scope.connectionsTotal = data.total;
  333. data = data.connections;
  334. for (id in data) {
  335. if (!data.hasOwnProperty(id)) {
  336. continue;
  337. }
  338. try {
  339. data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
  340. data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
  341. } catch (e) {
  342. data[id].inbps = 0;
  343. data[id].outbps = 0;
  344. }
  345. }
  346. $scope.connections = data;
  347. console.log("refreshConnections", data);
  348. }).error($scope.emitHTTPError);
  349. }
  350. function refreshErrors() {
  351. $http.get(urlbase + '/system/error').success(function (data) {
  352. $scope.errors = data.errors;
  353. console.log("refreshErrors", data);
  354. }).error($scope.emitHTTPError);
  355. }
  356. function refreshConfig() {
  357. $http.get(urlbase + '/system/config').success(function (data) {
  358. updateLocalConfig(data);
  359. console.log("refreshConfig", data);
  360. }).error($scope.emitHTTPError);
  361. $http.get(urlbase + '/system/config/insync').success(function (data) {
  362. $scope.configInSync = data.configInSync;
  363. }).error($scope.emitHTTPError);
  364. }
  365. function refreshNeed(folder) {
  366. var url = urlbase + "/db/need?folder=" + encodeURIComponent(folder);
  367. url += "&page=" + $scope.neededCurrentPage;
  368. url += "&perpage=" + $scope.neededPageSize;
  369. $http.get(url).success(function (data) {
  370. if ($scope.neededFolder == folder) {
  371. console.log("refreshNeed", folder, data);
  372. parseNeeded(data);
  373. }
  374. }).error($scope.emitHTTPError);
  375. }
  376. function needAction(file) {
  377. var fDelete = 4096;
  378. var fDirectory = 16384;
  379. if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  380. return 'rmdir';
  381. } else if ((file.flags & fDelete) === fDelete) {
  382. return 'rm';
  383. } else if ((file.flags & fDirectory) === fDirectory) {
  384. return 'touch';
  385. } else {
  386. return 'sync';
  387. }
  388. };
  389. function parseNeeded(data) {
  390. var merged = [];
  391. data.progress.forEach(function (item) {
  392. item.type = "progress";
  393. item.action = needAction(item);
  394. merged.push(item);
  395. });
  396. data.queued.forEach(function (item) {
  397. item.type = "queued";
  398. item.action = needAction(item);
  399. merged.push(item);
  400. });
  401. data.rest.forEach(function (item) {
  402. item.type = "rest";
  403. item.action = needAction(item);
  404. merged.push(item);
  405. });
  406. $scope.needed = merged;
  407. $scope.neededTotal = data.total;
  408. }
  409. $scope.neededPageChanged = function (page) {
  410. $scope.neededCurrentPage = page;
  411. refreshNeed($scope.neededFolder);
  412. };
  413. $scope.neededChangePageSize = function (perpage) {
  414. $scope.neededPageSize = perpage;
  415. refreshNeed($scope.neededFolder);
  416. }
  417. var refreshDeviceStats = debounce(function () {
  418. $http.get(urlbase + "/stats/device").success(function (data) {
  419. $scope.deviceStats = data;
  420. for (var device in $scope.deviceStats) {
  421. $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
  422. $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
  423. }
  424. console.log("refreshDeviceStats", data);
  425. }).error($scope.emitHTTPError);
  426. }, 2500);
  427. var refreshFolderStats = debounce(function () {
  428. $http.get(urlbase + "/stats/folder").success(function (data) {
  429. $scope.folderStats = data;
  430. for (var folder in $scope.folderStats) {
  431. if ($scope.folderStats[folder].lastFile) {
  432. $scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
  433. }
  434. }
  435. console.log("refreshfolderStats", data);
  436. }).error($scope.emitHTTPError);
  437. }, 2500);
  438. $scope.refresh = function () {
  439. refreshSystem();
  440. refreshConnectionStats();
  441. refreshErrors();
  442. };
  443. $scope.folderStatus = function (folderCfg) {
  444. if (typeof $scope.model[folderCfg.id] === 'undefined') {
  445. return 'unknown';
  446. }
  447. if (folderCfg.devices.length <= 1) {
  448. return 'unshared';
  449. }
  450. if ($scope.model[folderCfg.id].invalid) {
  451. return 'stopped';
  452. }
  453. if ($scope.model[folderCfg.id].state == 'error') {
  454. return 'stopped'; // legacy, the state is called "stopped" in the GUI
  455. }
  456. // after restart syncthing process state may be empty
  457. if (!$scope.model[folderCfg.id].state) {
  458. return 'unknown';
  459. }
  460. return '' + $scope.model[folderCfg.id].state;
  461. };
  462. $scope.folderClass = function (folderCfg) {
  463. if (typeof $scope.model[folderCfg.id] === 'undefined') {
  464. // Unknown
  465. return 'info';
  466. }
  467. if (folderCfg.devices.length <= 1) {
  468. // Unshared
  469. return 'warning';
  470. }
  471. if ($scope.model[folderCfg.id].invalid !== '') {
  472. // Errored
  473. return 'danger';
  474. }
  475. var state = '' + $scope.model[folderCfg.id].state;
  476. if (state == 'idle') {
  477. return 'success';
  478. }
  479. if (state == 'syncing') {
  480. return 'primary';
  481. }
  482. if (state == 'scanning') {
  483. return 'primary';
  484. }
  485. if (state == 'error') {
  486. return 'danger';
  487. }
  488. return 'info';
  489. };
  490. $scope.syncPercentage = function (folder) {
  491. if (typeof $scope.model[folder] === 'undefined') {
  492. return 100;
  493. }
  494. if ($scope.model[folder].globalBytes === 0) {
  495. return 100;
  496. }
  497. var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
  498. return Math.floor(pct);
  499. };
  500. $scope.deviceIcon = function (deviceCfg) {
  501. if ($scope.connections[deviceCfg.deviceID]) {
  502. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  503. return 'ok';
  504. } else {
  505. return 'refresh';
  506. }
  507. }
  508. return 'minus';
  509. };
  510. $scope.deviceStatus = function (deviceCfg) {
  511. if ($scope.deviceFolders(deviceCfg).length === 0) {
  512. return 'unused';
  513. }
  514. if ($scope.connections[deviceCfg.deviceID]) {
  515. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  516. return 'insync';
  517. } else {
  518. return 'syncing';
  519. }
  520. }
  521. // Disconnected
  522. return 'disconnected';
  523. };
  524. $scope.deviceClass = function (deviceCfg) {
  525. if ($scope.deviceFolders(deviceCfg).length === 0) {
  526. // Unused
  527. return 'warning';
  528. }
  529. if ($scope.connections[deviceCfg.deviceID]) {
  530. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  531. return 'success';
  532. } else {
  533. return 'primary';
  534. }
  535. }
  536. // Disconnected
  537. return 'info';
  538. };
  539. $scope.deviceAddr = function (deviceCfg) {
  540. var conn = $scope.connections[deviceCfg.deviceID];
  541. if (conn) {
  542. return conn.address;
  543. }
  544. return '?';
  545. };
  546. $scope.deviceCompletion = function (deviceCfg) {
  547. var conn = $scope.connections[deviceCfg.deviceID];
  548. if (conn) {
  549. return conn.completion + '%';
  550. }
  551. return '';
  552. };
  553. $scope.findDevice = function (deviceID) {
  554. var matches = $scope.devices.filter(function (n) {
  555. return n.deviceID == deviceID;
  556. });
  557. if (matches.length != 1) {
  558. return undefined;
  559. }
  560. return matches[0];
  561. };
  562. $scope.deviceName = function (deviceCfg) {
  563. if (typeof deviceCfg === 'undefined') {
  564. return "";
  565. }
  566. if (deviceCfg.name) {
  567. return deviceCfg.name;
  568. }
  569. return deviceCfg.deviceID.substr(0, 6);
  570. };
  571. $scope.thisDeviceName = function () {
  572. var device = $scope.thisDevice();
  573. if (typeof device === 'undefined') {
  574. return "(unknown device)";
  575. }
  576. if (device.name) {
  577. return device.name;
  578. }
  579. return device.deviceID.substr(0, 6);
  580. };
  581. $scope.editSettings = function () {
  582. // Make a working copy
  583. $scope.tmpOptions = angular.copy($scope.config.options);
  584. $scope.tmpOptions.urEnabled = ($scope.tmpOptions.urAccepted > 0);
  585. $scope.tmpOptions.deviceName = $scope.thisDevice().name;
  586. $scope.tmpOptions.autoUpgradeEnabled = ($scope.tmpOptions.autoUpgradeIntervalH > 0);
  587. $scope.tmpGUI = angular.copy($scope.config.gui);
  588. $('#settings').modal();
  589. };
  590. $scope.saveConfig = function () {
  591. var cfg = JSON.stringify($scope.config);
  592. var opts = {
  593. headers: {
  594. 'Content-Type': 'application/json'
  595. }
  596. };
  597. $http.post(urlbase + '/system/config', cfg, opts).success(function () {
  598. $http.get(urlbase + '/system/config/insync').success(function (data) {
  599. $scope.configInSync = data.configInSync;
  600. });
  601. }).error($scope.emitHTTPError);
  602. };
  603. $scope.saveSettings = function () {
  604. // Make sure something changed
  605. var changed = !angular.equals($scope.config.options, $scope.tmpOptions) || !angular.equals($scope.config.gui, $scope.tmpGUI);
  606. if (changed) {
  607. // Check if usage reporting has been enabled or disabled
  608. if ($scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted <= 0) {
  609. $scope.tmpOptions.urAccepted = 1000;
  610. } else if (!$scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted > 0) {
  611. $scope.tmpOptions.urAccepted = -1;
  612. }
  613. // Check if auto-upgrade has been enabled or disabled
  614. if ($scope.tmpOptions.autoUpgradeEnabled) {
  615. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  616. } else {
  617. $scope.tmpOptions.autoUpgradeIntervalH = 0;
  618. }
  619. // Check if protocol will need to be changed on restart
  620. if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
  621. $scope.protocolChanged = true;
  622. }
  623. // Apply new settings locally
  624. $scope.thisDevice().name = $scope.tmpOptions.deviceName;
  625. $scope.config.options = angular.copy($scope.tmpOptions);
  626. $scope.config.gui = angular.copy($scope.tmpGUI);
  627. ['listenAddress', 'globalAnnounceServers'].forEach(function (key) {
  628. $scope.config.options[key] = $scope.config.options[key + "Str"].split(/[ ,]+/).map(function (x) {
  629. return x.trim();
  630. });
  631. });
  632. $scope.saveConfig();
  633. }
  634. $('#settings').modal("hide");
  635. };
  636. $scope.restart = function () {
  637. restarting = true;
  638. $('#restarting').modal();
  639. $http.post(urlbase + '/system/restart');
  640. $scope.configInSync = true;
  641. // Switch webpage protocol if needed
  642. if ($scope.protocolChanged) {
  643. var protocol = 'http';
  644. if ($scope.config.gui.useTLS) {
  645. protocol = 'https';
  646. }
  647. setTimeout(function () {
  648. window.location.protocol = protocol;
  649. }, 2500);
  650. $scope.protocolChanged = false;
  651. }
  652. };
  653. $scope.upgrade = function () {
  654. restarting = true;
  655. $('#majorUpgrade').modal('hide');
  656. $('#upgrading').modal();
  657. $http.post(urlbase + '/system/upgrade').success(function () {
  658. $('#restarting').modal();
  659. $('#upgrading').modal('hide');
  660. }).error(function () {
  661. $('#upgrading').modal('hide');
  662. });
  663. };
  664. $scope.upgradeMajor = function () {
  665. $('#majorUpgrade').modal();
  666. };
  667. $scope.shutdown = function () {
  668. restarting = true;
  669. $http.post(urlbase + '/system/shutdown').success(function () {
  670. $('#shutdown').modal();
  671. }).error($scope.emitHTTPError);
  672. $scope.configInSync = true;
  673. };
  674. $scope.editDevice = function (deviceCfg) {
  675. $scope.currentDevice = $.extend({}, deviceCfg);
  676. $scope.editingExisting = true;
  677. $scope.editingSelf = (deviceCfg.deviceID == $scope.myID);
  678. $scope.currentDevice.addressesStr = deviceCfg.addresses.join(', ');
  679. if (!$scope.editingSelf) {
  680. $scope.currentDevice.selectedFolders = {};
  681. $scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
  682. $scope.currentDevice.selectedFolders[folder] = true;
  683. });
  684. }
  685. $scope.deviceEditor.$setPristine();
  686. $('#editDevice').modal();
  687. };
  688. $scope.idDevice = function () {
  689. $('#idqr').modal('show');
  690. };
  691. $scope.addDevice = function () {
  692. $http.get(urlbase + '/system/discovery')
  693. .success(function (registry) {
  694. $scope.discovery = registry;
  695. })
  696. .then(function () {
  697. $scope.currentDevice = {
  698. addressesStr: 'dynamic',
  699. compression: 'metadata',
  700. introducer: false,
  701. selectedFolders: {}
  702. };
  703. $scope.editingExisting = false;
  704. $scope.editingSelf = false;
  705. $scope.deviceEditor.$setPristine();
  706. $('#editDevice').modal();
  707. });
  708. };
  709. $scope.deleteDevice = function () {
  710. $('#editDevice').modal('hide');
  711. if (!$scope.editingExisting) {
  712. return;
  713. }
  714. $scope.devices = $scope.devices.filter(function (n) {
  715. return n.deviceID !== $scope.currentDevice.deviceID;
  716. });
  717. $scope.config.devices = $scope.devices;
  718. // In case we later added the device manually, remove the ignoral
  719. // record.
  720. $scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
  721. return id !== $scope.currentDevice.deviceID;
  722. });
  723. for (var id in $scope.folders) {
  724. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  725. return n.deviceID !== $scope.currentDevice.deviceID;
  726. });
  727. }
  728. $scope.saveConfig();
  729. };
  730. $scope.saveDevice = function () {
  731. $('#editDevice').modal('hide');
  732. $scope.saveDeviceConfig($scope.currentDevice);
  733. };
  734. $scope.addNewDeviceID = function (device) {
  735. var deviceCfg = {
  736. deviceID: device,
  737. addressesStr: 'dynamic',
  738. compression: 'metadata',
  739. introducer: false,
  740. selectedFolders: {}
  741. };
  742. $scope.saveDeviceConfig(deviceCfg);
  743. $scope.dismissDeviceRejection(device);
  744. };
  745. $scope.saveDeviceConfig = function (deviceCfg) {
  746. var done, i;
  747. deviceCfg.addresses = deviceCfg.addressesStr.split(',').map(function (x) {
  748. return x.trim();
  749. });
  750. done = false;
  751. for (i = 0; i < $scope.devices.length; i++) {
  752. if ($scope.devices[i].deviceID === deviceCfg.deviceID) {
  753. $scope.devices[i] = deviceCfg;
  754. done = true;
  755. break;
  756. }
  757. }
  758. if (!done) {
  759. $scope.devices.push(deviceCfg);
  760. }
  761. $scope.devices.sort(deviceCompare);
  762. $scope.config.devices = $scope.devices;
  763. // In case we are adding the device manually, remove the ignoral
  764. // record.
  765. $scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
  766. return id !== deviceCfg.deviceID;
  767. });
  768. if (!$scope.editingSelf) {
  769. for (var id in deviceCfg.selectedFolders) {
  770. if (deviceCfg.selectedFolders[id]) {
  771. var found = false;
  772. for (i = 0; i < $scope.folders[id].devices.length; i++) {
  773. if ($scope.folders[id].devices[i].deviceID == deviceCfg.deviceID) {
  774. found = true;
  775. break;
  776. }
  777. }
  778. if (!found) {
  779. $scope.folders[id].devices.push({
  780. deviceID: deviceCfg.deviceID
  781. });
  782. }
  783. } else {
  784. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  785. return n.deviceID != deviceCfg.deviceID;
  786. });
  787. }
  788. }
  789. }
  790. $scope.saveConfig();
  791. };
  792. $scope.dismissDeviceRejection = function (device) {
  793. delete $scope.deviceRejections[device];
  794. };
  795. $scope.ignoreRejectedDevice = function (device) {
  796. $scope.config.ignoredDevices.push(device);
  797. $scope.saveConfig();
  798. $scope.dismissDeviceRejection(device);
  799. };
  800. $scope.otherDevices = function () {
  801. return $scope.devices.filter(function (n) {
  802. return n.deviceID !== $scope.myID;
  803. });
  804. };
  805. $scope.thisDevice = function () {
  806. var i, n;
  807. for (i = 0; i < $scope.devices.length; i++) {
  808. n = $scope.devices[i];
  809. if (n.deviceID === $scope.myID) {
  810. return n;
  811. }
  812. }
  813. };
  814. $scope.allDevices = function () {
  815. var devices = $scope.otherDevices();
  816. devices.push($scope.thisDevice());
  817. return devices;
  818. };
  819. $scope.errorList = function () {
  820. return $scope.errors.filter(function (e) {
  821. return e.time > $scope.seenError;
  822. });
  823. };
  824. $scope.clearErrors = function () {
  825. $scope.seenError = $scope.errors[$scope.errors.length - 1].time;
  826. $http.post(urlbase + '/system/error/clear');
  827. };
  828. $scope.friendlyDevices = function (str) {
  829. for (var i = 0; i < $scope.devices.length; i++) {
  830. var cfg = $scope.devices[i];
  831. str = str.replace(cfg.deviceID, $scope.deviceName(cfg));
  832. }
  833. return str;
  834. };
  835. $scope.folderList = function () {
  836. return folderList($scope.folders);
  837. };
  838. $scope.directoryList = [];
  839. $scope.$watch('currentFolder.path', function (newvalue) {
  840. if (newvalue && newvalue.trim().charAt(0) == '~') {
  841. $scope.currentFolder.path = $scope.system.tilde + newvalue.trim().substring(1)
  842. }
  843. $http.get(urlbase + '/system/browse', {
  844. params: { current: newvalue }
  845. }).success(function (data) {
  846. $scope.directoryList = data;
  847. }).error($scope.emitHTTPError);
  848. });
  849. $scope.editFolder = function (folderCfg) {
  850. $scope.currentFolder = angular.copy(folderCfg);
  851. if ($scope.currentFolder.path.slice(-1) == $scope.system.pathSeparator) {
  852. $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
  853. }
  854. $scope.currentFolder.selectedDevices = {};
  855. $scope.currentFolder.devices.forEach(function (n) {
  856. $scope.currentFolder.selectedDevices[n.deviceID] = true;
  857. });
  858. if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "trashcan") {
  859. $scope.currentFolder.trashcanFileVersioning = true;
  860. $scope.currentFolder.fileVersioningSelector = "trashcan";
  861. $scope.currentFolder.trashcanClean = +$scope.currentFolder.versioning.params.cleanoutDays;
  862. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "simple") {
  863. $scope.currentFolder.simpleFileVersioning = true;
  864. $scope.currentFolder.fileVersioningSelector = "simple";
  865. $scope.currentFolder.simpleKeep = +$scope.currentFolder.versioning.params.keep;
  866. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "staggered") {
  867. $scope.currentFolder.staggeredFileVersioning = true;
  868. $scope.currentFolder.fileVersioningSelector = "staggered";
  869. $scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.versioning.params.maxAge / 86400);
  870. $scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.versioning.params.cleanInterval;
  871. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.versioning.params.versionsPath;
  872. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "external") {
  873. $scope.currentFolder.externalFileVersioning = true;
  874. $scope.currentFolder.fileVersioningSelector = "external";
  875. $scope.currentFolder.externalCommand = $scope.currentFolder.versioning.params.command;
  876. } else {
  877. $scope.currentFolder.fileVersioningSelector = "none";
  878. }
  879. $scope.currentFolder.trashcanClean = $scope.currentFolder.trashcanClean || 0; // weeds out nulls and undefineds
  880. $scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5;
  881. $scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600;
  882. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.staggeredVersionsPath || "";
  883. // staggeredMaxAge can validly be zero, which we should not replace
  884. // with the default value of 365. So only set the default if it's
  885. // actually undefined.
  886. if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') {
  887. $scope.currentFolder.staggeredMaxAge = 365;
  888. }
  889. $scope.currentFolder.externalCommand = $scope.currentFolder.externalCommand || "";
  890. $scope.editingExisting = true;
  891. $scope.folderEditor.$setPristine();
  892. $('#editFolder').modal();
  893. };
  894. $scope.addFolder = function () {
  895. $scope.currentFolder = {
  896. selectedDevices: {}
  897. };
  898. $scope.currentFolder.rescanIntervalS = 60;
  899. $scope.currentFolder.fileVersioningSelector = "none";
  900. $scope.currentFolder.trashcanClean = 0;
  901. $scope.currentFolder.simpleKeep = 5;
  902. $scope.currentFolder.staggeredMaxAge = 365;
  903. $scope.currentFolder.staggeredCleanInterval = 3600;
  904. $scope.currentFolder.staggeredVersionsPath = "";
  905. $scope.currentFolder.externalCommand = "";
  906. $scope.currentFolder.autoNormalize = true;
  907. $scope.editingExisting = false;
  908. $scope.folderEditor.$setPristine();
  909. $('#editFolder').modal();
  910. };
  911. $scope.addFolderAndShare = function (folder, device) {
  912. $scope.dismissFolderRejection(folder, device);
  913. $scope.currentFolder = {
  914. id: folder,
  915. selectedDevices: {}
  916. };
  917. $scope.currentFolder.selectedDevices[device] = true;
  918. $scope.currentFolder.rescanIntervalS = 60;
  919. $scope.currentFolder.fileVersioningSelector = "none";
  920. $scope.currentFolder.trashcanClean = 0;
  921. $scope.currentFolder.simpleKeep = 5;
  922. $scope.currentFolder.staggeredMaxAge = 365;
  923. $scope.currentFolder.staggeredCleanInterval = 3600;
  924. $scope.currentFolder.staggeredVersionsPath = "";
  925. $scope.currentFolder.externalCommand = "";
  926. $scope.currentFolder.autoNormalize = true;
  927. $scope.editingExisting = false;
  928. $scope.folderEditor.$setPristine();
  929. $('#editFolder').modal();
  930. };
  931. $scope.shareFolderWithDevice = function (folder, device) {
  932. $scope.folders[folder].devices.push({
  933. deviceID: device
  934. });
  935. $scope.config.folders = folderList($scope.folders);
  936. $scope.saveConfig();
  937. $scope.dismissFolderRejection(folder, device);
  938. };
  939. $scope.saveFolder = function () {
  940. var folderCfg, done, i;
  941. $('#editFolder').modal('hide');
  942. folderCfg = $scope.currentFolder;
  943. folderCfg.devices = [];
  944. folderCfg.selectedDevices[$scope.myID] = true;
  945. for (var deviceID in folderCfg.selectedDevices) {
  946. if (folderCfg.selectedDevices[deviceID] === true) {
  947. folderCfg.devices.push({
  948. deviceID: deviceID
  949. });
  950. }
  951. }
  952. delete folderCfg.selectedDevices;
  953. if (folderCfg.fileVersioningSelector === "trashcan") {
  954. folderCfg.versioning = {
  955. 'Type': 'trashcan',
  956. 'Params': {
  957. 'cleanoutDays': '' + folderCfg.trashcanClean
  958. }
  959. };
  960. delete folderCfg.trashcanFileVersioning;
  961. delete folderCfg.trashcanClean;
  962. } else if (folderCfg.fileVersioningSelector === "simple") {
  963. folderCfg.versioning = {
  964. 'Type': 'simple',
  965. 'Params': {
  966. 'keep': '' + folderCfg.simpleKeep
  967. }
  968. };
  969. delete folderCfg.simpleFileVersioning;
  970. delete folderCfg.simpleKeep;
  971. } else if (folderCfg.fileVersioningSelector === "staggered") {
  972. folderCfg.versioning = {
  973. 'type': 'staggered',
  974. 'params': {
  975. 'maxAge': '' + (folderCfg.staggeredMaxAge * 86400),
  976. 'cleanInterval': '' + folderCfg.staggeredCleanInterval,
  977. 'versionsPath': '' + folderCfg.staggeredVersionsPath
  978. }
  979. };
  980. delete folderCfg.staggeredFileVersioning;
  981. delete folderCfg.staggeredMaxAge;
  982. delete folderCfg.staggeredCleanInterval;
  983. delete folderCfg.staggeredVersionsPath;
  984. } else if (folderCfg.fileVersioningSelector === "external") {
  985. folderCfg.versioning = {
  986. 'Type': 'external',
  987. 'Params': {
  988. 'command': '' + folderCfg.externalCommand
  989. }
  990. };
  991. delete folderCfg.externalFileVersioning;
  992. delete folderCfg.externalCommand;
  993. } else {
  994. delete folderCfg.versioning;
  995. }
  996. $scope.folders[folderCfg.id] = folderCfg;
  997. $scope.config.folders = folderList($scope.folders);
  998. $scope.saveConfig();
  999. };
  1000. $scope.dismissFolderRejection = function (folder, device) {
  1001. delete $scope.folderRejections[folder + "-" + device];
  1002. };
  1003. $scope.sharesFolder = function (folderCfg) {
  1004. var names = [];
  1005. folderCfg.devices.forEach(function (device) {
  1006. if (device.deviceID != $scope.myID) {
  1007. names.push($scope.deviceName($scope.findDevice(device.deviceID)));
  1008. }
  1009. });
  1010. names.sort();
  1011. return names.join(", ");
  1012. }
  1013. $scope.deviceFolders = function (deviceCfg) {
  1014. var folders = [];
  1015. for (var folderID in $scope.folders) {
  1016. var devices = $scope.folders[folderID].devices
  1017. for (var i = 0; i < devices.length; i++) {
  1018. if (devices[i].deviceID == deviceCfg.deviceID) {
  1019. folders.push(folderID);
  1020. break;
  1021. }
  1022. }
  1023. }
  1024. folders.sort();
  1025. return folders;
  1026. };
  1027. $scope.deleteFolder = function () {
  1028. $('#editFolder').modal('hide');
  1029. if (!$scope.editingExisting) {
  1030. return;
  1031. }
  1032. delete $scope.folders[$scope.currentFolder.id];
  1033. $scope.config.folders = folderList($scope.folders);
  1034. $scope.saveConfig();
  1035. };
  1036. $scope.editIgnores = function () {
  1037. if (!$scope.editingExisting) {
  1038. return;
  1039. }
  1040. $('#editIgnoresButton').attr('disabled', 'disabled');
  1041. $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
  1042. .success(function (data) {
  1043. data.ignore = data.ignore || [];
  1044. $('#editFolder').modal('hide');
  1045. var textArea = $('#editIgnores textarea');
  1046. textArea.val(data.ignore.join('\n'));
  1047. $('#editIgnores').modal()
  1048. .on('hidden.bs.modal', function () {
  1049. $('#editFolder').modal();
  1050. })
  1051. .on('shown.bs.modal', function () {
  1052. textArea.focus();
  1053. });
  1054. })
  1055. .then(function () {
  1056. $('#editIgnoresButton').removeAttr('disabled');
  1057. });
  1058. };
  1059. $scope.saveIgnores = function () {
  1060. if (!$scope.editingExisting) {
  1061. return;
  1062. }
  1063. $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
  1064. ignore: $('#editIgnores textarea').val().split('\n')
  1065. });
  1066. };
  1067. $scope.setAPIKey = function (cfg) {
  1068. cfg.apiKey = randomString(32);
  1069. };
  1070. $scope.showURPreview = function () {
  1071. $('#settings').modal('hide');
  1072. $('#urPreview').modal().on('hidden.bs.modal', function () {
  1073. $('#settings').modal();
  1074. });
  1075. };
  1076. $scope.acceptUR = function () {
  1077. $scope.config.options.urAccepted = 1000; // Larger than the largest existing report version
  1078. $scope.saveConfig();
  1079. $('#ur').modal('hide');
  1080. };
  1081. $scope.declineUR = function () {
  1082. $scope.config.options.urAccepted = -1;
  1083. $scope.saveConfig();
  1084. $('#ur').modal('hide');
  1085. };
  1086. $scope.showNeed = function (folder) {
  1087. $scope.neededFolder = folder;
  1088. refreshNeed(folder);
  1089. $('#needed').modal().on('hidden.bs.modal', function () {
  1090. $scope.neededFolder = undefined;
  1091. $scope.needed = undefined;
  1092. $scope.neededTotal = 0;
  1093. $scope.neededCurrentPage = 1;
  1094. });
  1095. };
  1096. $scope.override = function (folder) {
  1097. $http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
  1098. };
  1099. $scope.about = function () {
  1100. $('#about').modal('show');
  1101. };
  1102. $scope.showReportPreview = function () {
  1103. $scope.reportPreview = true;
  1104. };
  1105. $scope.rescanAllFolders = function () {
  1106. $http.post(urlbase + "/db/scan");
  1107. };
  1108. $scope.rescanFolder = function (folder) {
  1109. $http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
  1110. };
  1111. $scope.bumpFile = function (folder, file) {
  1112. var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
  1113. // In order to get the right view of data in the response.
  1114. url += "&page=" + $scope.neededCurrentPage;
  1115. url += "&perpage=" + $scope.neededPageSize;
  1116. $http.post(url).success(function (data) {
  1117. if ($scope.neededFolder == folder) {
  1118. console.log("bumpFile", folder, data);
  1119. parseNeeded(data);
  1120. }
  1121. }).error($scope.emitHTTPError);
  1122. };
  1123. $scope.versionString = function () {
  1124. if (!$scope.version.version) {
  1125. return '';
  1126. }
  1127. var os = {
  1128. 'darwin': 'Mac OS X',
  1129. 'dragonfly': 'DragonFly BSD',
  1130. 'freebsd': 'FreeBSD',
  1131. 'openbsd': 'OpenBSD',
  1132. 'netbsd': 'NetBSD',
  1133. 'linux': 'Linux',
  1134. 'windows': 'Windows',
  1135. 'solaris': 'Solaris',
  1136. }[$scope.version.os] || $scope.version.os;
  1137. var arch ={
  1138. '386': '32 bit',
  1139. 'amd64': '64 bit',
  1140. 'arm': 'ARM',
  1141. }[$scope.version.arch] || $scope.version.arch;
  1142. return $scope.version.version + ', ' + os + ' (' + arch + ')';
  1143. };
  1144. // pseudo main. called on all definitions assigned
  1145. initController();
  1146. });