'use strict'; /* global $, async, angular, redirectIfNeeded */ /* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */ // deal with accessToken in the query, this is passed for example on password reset and account setup upon invite var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); if (search.accessToken) { localStorage.token = search.accessToken; // strip the accessToken and expiresAt, then preserve the rest delete search.accessToken; delete search.expiresAt; // this will reload the page as this is not a hash change window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&')); } // create main application module var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.multiselect']); app.config(['NotificationProvider', function (NotificationProvider) { NotificationProvider.setOptions({ delay: 5000, startTop: 60, positionX: 'left', templateUrl: 'notification.html' }); }]); // configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist app.config(function ($sceDelegateProvider) { $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow loading from our assets domain. 'https://cloudron.io/**', 'https://staging.cloudron.io/**', 'https://dev.cloudron.io/**', // Allow local development against the appstore pages 'http://localhost:5000/**' ]); }); // setup all major application routes app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/', { redirectTo: '/apps' }).when('/users', { // controller: 'UsersController', // templateUrl: 'views/users.html?' + window.VITE_CACHE_ID }).when('/user-directory', { // controller: 'UserSettingsController', // templateUrl: 'views/user-directory.html?' + window.VITE_CACHE_ID }).when('/app/:appId/:view?', { controller: 'AppController', templateUrl: 'views/app.html?' + window.VITE_CACHE_ID }).when('/appstore', { // controller: 'AppStoreController', // templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID }).when('/appstore/:appId', { // controller: 'AppStoreController', // templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID }).when('/apps', { // controller: 'AppsController', // templateUrl: 'views/apps.html?' + window.VITE_CACHE_ID }).when('/profile', { // controller: 'ProfileController', // templateUrl: 'views/profile.html?' + window.VITE_CACHE_ID }).when('/backups', { // controller: 'BackupsController', // templateUrl: 'views/backups.html?' + window.VITE_CACHE_ID }).when('/branding', { // controller: 'BrandingController', // templateUrl: 'views/branding.html?' + window.VITE_CACHE_ID }).when('/network', { // controller: 'NetworkController', // templateUrl: 'views/network.html?' + window.VITE_CACHE_ID }).when('/domains', { // controller: 'DomainsController', // templateUrl: 'views/domains.html?' + window.VITE_CACHE_ID }).when('/email', { controller: 'EmailsController', templateUrl: 'views/emails.html?' + window.VITE_CACHE_ID }).when('/emails-eventlog', { controller: 'EmailsEventlogController', templateUrl: 'views/emails-eventlog.html?' + window.VITE_CACHE_ID }).when('/emails-queue', { controller: 'EmailsQueueController', templateUrl: 'views/emails-queue.html?' + window.VITE_CACHE_ID }).when('/email/:domain/:view?', { controller: 'EmailController', templateUrl: 'views/email.html?' + window.VITE_CACHE_ID }).when('/notifications', { controller: 'NotificationsController', templateUrl: 'views/notifications.html?' + window.VITE_CACHE_ID }).when('/oidc', { redirectTo: '/user-directory' }).when('/settings', { // controller: 'SettingsController', // templateUrl: 'views/settings.html?' + window.VITE_CACHE_ID }).when('/eventlog', { // controller: 'EventLogController', // templateUrl: 'views/eventlog.html?' + window.VITE_CACHE_ID }).when('/support', { // controller: 'SupportController', // templateUrl: 'views/support.html?' + window.VITE_CACHE_ID }).when('/system', { controller: 'SystemController', templateUrl: 'views/system.html?' + window.VITE_CACHE_ID }).when('/services', { // controller: 'ServicesController', // templateUrl: 'views/services.html?' + window.VITE_CACHE_ID }).when('/volumes', { // controller: 'VolumesController', // templateUrl: 'views/volumes.html?' + window.VITE_CACHE_ID }).otherwise({ redirectTo: '/'}); }]); app.filter('notificationTypeToColor', function () { return function (n) { switch (n.type) { case NOTIFICATION_TYPES.REBOOT: case NOTIFICATION_TYPES.APP_OOM: case NOTIFICATION_TYPES.MAIL_STATUS: case NOTIFICATION_TYPES.CERTIFICATE_RENEWAL_FAILED: case NOTIFICATION_TYPES.DISK_SPACE: case NOTIFICATION_TYPES.BACKUP_CONFIG: case NOTIFICATION_TYPES.BACKUP_FAILED: return '#ff4c4c'; case NOTIFICATION_TYPES.BOX_UPDATE: case NOTIFICATION_TYPES.MANUAL_APP_UPDATE: return '#f0ad4e'; default: return '#2196f3'; } }; }); app.filter('capitalize', function () { return function (s) { return s.charAt(0).toUpperCase() + s.slice(1); }; }); app.filter('activeTask', function () { return function (app) { if (!app) return false; return app.taskId !== null; }; }); app.filter('installSuccess', function () { return function (app) { if (!app) return false; return app.installationState === ISTATES.INSTALLED; }; }); app.filter('appIsInstalledAndHealthy', function () { return function (app) { if (!app) return false; return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING); }; }); app.filter('applicationLink', function () { return function(app) { if (!app) return ''; // app links have http already in the fqdn if (app.fqdn.indexOf('http') !== 0) return 'https://' + app.fqdn; return app.fqdn; }; }); app.filter('userManagementFilter', function () { return function(apps, option) { return apps.filter(function (app) { if (option.id === '') return true; if (option.id === 'sso') return !!(app.manifest.optionalSso || app.manifest.addons.ldap || app.manifest.addons.proxyAuth); if (option.id === 'nosso') return app.manifest.optionalSso || (!app.manifest.addons.ldap && !app.manifest.addons.proxyAuth); if (option.id === 'email') return !!app.manifest.addons.email; return false; }); }; }); // this appears when an item in app grid is clicked app.filter('prettyAppErrorMessage', function () { return function (app) { if (!app) return ''; if (app.installationState === ISTATES.INSTALLED) { // app.health can also be null to indicate insufficient data if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.'; } if (app.error.reason === 'Access Denied') { if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.'; } else if (app.error.reason === 'Already Exists') { if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.'; } return app.error.message; }; }); // this appears as tool tip in app grid app.filter('appProgressMessage', function () { return function (app) { var message = app.message || (app.error ? app.error.message : ''); return message; }; }); // see apps.js $scope.states app.filter('selectedStateFilter', ['Client', function (Client) { return function selectedStateFilter(apps, selectedState) { return apps.filter(function (app) { if (!selectedState || !selectedState.state) return true; if (selectedState.state === 'running') return app.runState === RSTATES.RUNNING && app.health === HSTATES.HEALTHY && app.installationState === ISTATES.INSTALLED; if (selectedState.state === 'stopped') return app.runState === RSTATES.STOPPED; if (selectedState.state === 'update_available') return !!(Client.getConfig().update[app.id] && Client.getConfig().update[app.id].manifest.version && Client.getConfig().update[app.id].manifest.version !== app.manifest.version); return app.runState === RSTATES.RUNNING && (app.health !== HSTATES.HEALTHY || app.installationState !== ISTATES.INSTALLED); // not responding }); }; }]); app.filter('selectedGroupAccessFilter', function () { return function selectedGroupAccessFilter(apps, group) { return apps.filter(function (app) { if (!group.id) return true; // case for no filter entry if (!app.accessRestriction) return true; if (!app.accessRestriction.groups) return false; if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true; return false; }); }; }); app.filter('selectedTagFilter', function () { return function selectedTagFilter(apps, selectedTags) { return apps.filter(function (app) { if (selectedTags.length === 0) return true; if (!app.tags) return false; for (var i = 0; i < selectedTags.length; i++) { if (app.tags.indexOf(selectedTags[i]) === -1) return false; } return true; }); }; }); app.filter('selectedDomainFilter', function () { return function selectedDomainFilter(apps, selectedDomain) { return apps.filter(function (app) { if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN if (app.type === APP_TYPES.LINK) return false; if (selectedDomain.domain === app.domain) return true; if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true; if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true; return false; }); }; }); app.filter('appSearchFilter', function () { return function appSearchFilter(apps, appSearch) { return apps.filter(function (app) { if (!appSearch) return true; appSearch = appSearch.toLowerCase(); return app.fqdn.indexOf(appSearch) !== -1 || (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1) || (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1) || (appSearch.length >=6 && app.id.indexOf(appSearch) !== -1); }); }; }); app.filter('prettyDomains', function () { return function prettyDomains(domains) { return domains.map(function (d) { return d.domain; }).join(', '); }; }); app.filter('installationActive', function () { return function (app) { if (app.installationState === ISTATES.ERROR) return false; if (app.installationState === ISTATES.INSTALLED) return false; return true; }; }); // color indicator in app list app.filter('installationStateClass', function () { const ERROR_CLASS = 'status-error'; const BUSY_CLASS = 'status-starting fa-beat-fade'; const INACTIVE_CLASS = 'status-inactive'; const ACTIVE_CLASS = 'status-active'; return function(app) { if (!app) return ''; switch (app.installationState) { case ISTATES.ERROR: return ERROR_CLASS; case ISTATES.INSTALLED: { if (app.debugMode) { return INACTIVE_CLASS; } else { if (app.runState === RSTATES.RUNNING) { if (!app.health) return BUSY_CLASS; // no data yet if (app.type === APP_TYPES.LINK || app.health === HSTATES.HEALTHY) return ACTIVE_CLASS; return ERROR_CLASS; // dead/exit/unhealthy } else { return INACTIVE_CLASS; } } } default: return BUSY_CLASS; } }; }); // this appears in the app grid app.filter('installationStateLabel', function () { return function(app) { if (!app) return ''; var waiting = app.progress === 0 ? ' (Queued)' : ''; switch (app.installationState) { case ISTATES.PENDING_INSTALL: return 'Installing' + waiting; case ISTATES.PENDING_CLONE: return 'Cloning' + waiting; case ISTATES.PENDING_LOCATION_CHANGE: case ISTATES.PENDING_CONFIGURE: case ISTATES.PENDING_RECREATE_CONTAINER: case ISTATES.PENDING_SERVICES_CHANGE: case ISTATES.PENDING_DEBUG: return 'Configuring' + waiting; case ISTATES.PENDING_RESIZE: return 'Resizing' + waiting; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'Migrating data' + waiting; case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting; case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting; case ISTATES.PENDING_IMPORT: return 'Importing' + waiting; case ISTATES.PENDING_UPDATE: return 'Updating' + waiting; case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting; case ISTATES.PENDING_START: return 'Starting' + waiting; case ISTATES.PENDING_STOP: return 'Stopping' + waiting; case ISTATES.PENDING_RESTART: return 'Restarting' + waiting; case ISTATES.ERROR: { if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error'; return 'Error'; } case ISTATES.INSTALLED: { if (app.debugMode) { return 'Recovery Mode'; } else if (app.runState === RSTATES.RUNNING) { if (!app.health) return 'Starting...'; // no data yet if (app.type === APP_TYPES.LINK) return ''; if (app.health === HSTATES.HEALTHY) return 'Running'; return 'Not responding'; // dead/exit/unhealthy } else if (app.runState === RSTATES.STOPPED) return 'Stopped'; else return app.runState; } default: return app.installationState; } }; }); app.filter('taskName', function () { return function(installationState) { switch (installationState) { case ISTATES.PENDING_INSTALL: return 'install'; case ISTATES.PENDING_CLONE: return 'clone'; case ISTATES.PENDING_LOCATION_CHANGE: return 'location change'; case ISTATES.PENDING_CONFIGURE: return 'configure'; case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container'; case ISTATES.PENDING_DEBUG: return 'debug'; case ISTATES.PENDING_RESIZE: return 'resize'; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration'; case ISTATES.PENDING_UNINSTALL: return 'uninstall'; case ISTATES.PENDING_RESTORE: return 'restore'; case ISTATES.PENDING_IMPORT: return 'import'; case ISTATES.PENDING_UPDATE: return 'update'; case ISTATES.PENDING_BACKUP: return 'backup'; case ISTATES.PENDING_START: return 'start app'; case ISTATES.PENDING_STOP: return 'stop app'; case ISTATES.PENDING_RESTART: return 'restart app'; default: return installationState || ''; } }; }); app.filter('errorSuggestion', function () { return function (error) { if (!error) return ''; switch (error.reason) { case ERROR.ACCESS_DENIED: if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view'; return ''; case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server'; case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server'; case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server'; case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running'; case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server'; case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server'; case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server'; default: return ''; } }; }); app.filter('canUpdate', function () { return function (apps) { return apps.every(function (app) { return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED); }); }; }); app.filter('inProgressApps', function () { return function (apps) { return apps.filter(function (app) { return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED; }); }; }); app.filter('prettyHref', function () { return function (input) { if (!input) return input; if (input.indexOf('http://') === 0) return input.slice('http://'.length); if (input.indexOf('https://') === 0) return input.slice('https://'.length); return input; }; }); app.filter('prettyEmailAddresses', function () { return function prettyEmailAddresses(addresses) { if (!addresses) return ''; if (addresses === '<>') return '<>'; if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', '); return addresses.slice(1, -1); }; }); // custom directive for dynamic names in forms // See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401 app.directive('laterName', function () { // (2) return { restrict: 'A', require: ['?ngModel', '^?form'], // (3) link: function postLink(scope, elem, attrs, ctrls) { attrs.$set('name', attrs.laterName); var modelCtrl = ctrls[0]; // (3) var formCtrl = ctrls[1]; // (3) if (modelCtrl && formCtrl) { modelCtrl.$name = attrs.name; // (4) formCtrl.$addControl(modelCtrl); // (2) scope.$on('$destroy', function () { formCtrl.$removeControl(modelCtrl); // (5) }); } } }; }); app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) { var original = $location.path; $location.path = function (path, reload) { if (reload === false) { var lastRoute = $route.current; var un = $rootScope.$on('$locationChangeSuccess', function () { $route.current = lastRoute; un(); }); } return original.apply($location, [path]); }; }]); app.directive('ngClickSelect', function () { return { restrict: 'AC', link: function (scope, element/*, attrs */) { element.bind('click', function () { var selection = window.getSelection(); var range = document.createRange(); range.selectNodeContents(this); selection.removeAllRanges(); selection.addRange(range); }); } }; }); // handles various states and triggers a href or configure/repair view // used by attaching to controller $scope // if $scope.appPostInstallConfirm.show(app); exists it will be called if not yet confirmed function onAppClick(app, $event, isOperator, $scope) { function stopEvent() { $event.originalEvent.stopPropagation(); $event.originalEvent.preventDefault(); } if (app.installationState !== ISTATES.INSTALLED) { if (app.installationState === ISTATES.ERROR && isOperator) $scope.showAppConfigure(app, 'repair'); return stopEvent(); } // app.health can also be null to indicate insufficient data if (!app.health) return stopEvent(); if (app.runState === RSTATES.STOPPED) return stopEvent(); if (app.runState === RSTATES.STOPPED) return stopEvent(); if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) { if (isOperator) $scope.showAppConfigure(app, 'repair'); return stopEvent(); } if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) { $scope.appPostInstallConfirm.show(app); return stopEvent(); } } app.directive('ngClickReveal', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.addClass('hand'); var value = ''; scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) { if (newValue !== oldValue) { element.html('hidden'); value = newValue; } }); element.bind('click', function () { element.text(value); }); } }; }); // https://codepen.io/webmatze/pen/isuHh app.directive('tagInput', function () { return { restrict: 'E', scope: { inputTags: '=taglist' }, require: '^form', link: function ($scope, element, attrs, formCtrl) { $scope.defaultWidth = 200; $scope.tagText = ''; // current tag being edited $scope.placeholder = attrs.placeholder; $scope.tagArray = function () { if ($scope.inputTags === undefined) { return []; } return $scope.inputTags.split(' ').filter(function (tag) { return tag !== ''; }); }; $scope.addTag = function () { var tagArray = $scope.tagArray(); // prevent adding empty or existing items if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) { return $scope.tagText = ''; } tagArray.push($scope.tagText); $scope.inputTags = tagArray.join(' '); return $scope.tagText = ''; }; $scope.deleteTag = function (key) { var tagArray; tagArray = $scope.tagArray(); if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) { tagArray.pop(); } else { if (key !== undefined) { tagArray.splice(key, 1); } } formCtrl.$setDirty(); return $scope.inputTags = tagArray.join(' '); }; $scope.$watch('tagText', function (newVal, oldVal) { var tempEl; if (!(newVal === oldVal && newVal === undefined)) { tempEl = $('' + newVal + '').appendTo('body'); $scope.inputWidth = tempEl.width() + 5; if ($scope.inputWidth < $scope.defaultWidth) { $scope.inputWidth = $scope.defaultWidth; } return tempEl.remove(); } }); element.bind('click', function () { element[0].firstChild.lastChild.focus(); }); element.bind('keydown', function (e) { var key = e.which; if (key === 9 || key === 13) { e.preventDefault(); } if (key === 8) { return $scope.$apply('deleteTag()'); } }); element.bind('keyup', function (e) { var key = e.which; if (key === 9 || key === 13 || key === 32) { e.preventDefault(); return $scope.$apply('addTag()'); } }); }, template: '
' + '
' + '' + '' + '
' + '' + '
' }; }); app.config(['fitTextConfigProvider', function (fitTextConfigProvider) { fitTextConfigProvider.config = { loadDelay: 250, compressor: 0.9, min: 8, max: 24 }; }]); app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) { $scope.initialized = false; // used to animate the UI $scope.user = Client.getUserInfo(); $scope.installedApps = Client.getInstalledApps(); $scope.config = {}; $scope.client = Client; $scope.subscription = {}; $scope.notificationCount = 0; $scope.hideNavBarActions = $location.path() === '/logs'; $scope.backgroundImageUrl = ''; $scope.sideBarOpen = false; $scope.closeNavbar = function () { $scope.sideBarOpen = false; }; $scope.onSidebarToggle = function () { $scope.sideBarOpen = !$scope.sideBarOpen; }; $scope.reboot = { busy: false, show: function () { $scope.reboot.busy = false; $('#rebootModal').modal('show'); }, submit: function () { $scope.reboot.busy = true; Client.reboot(function (error) { if (error) return Client.error(error); $('#rebootModal').modal('hide'); // trigger refetch to show offline banner $timeout(function () { Client.getStatus(function () {}); }, 5000); }); } }; $scope.isActive = function (url) { if (!$route.current) return false; return $route.current.$$route.originalPath === url; }; $scope.logout = function (event) { event.stopPropagation(); $scope.initialized = false; Client.logout(); }; $scope.openSubscriptionSetup = function () { Client.openSubscriptionSetup($scope.subscription); }; // NOTE: this function is exported and called from the appstore.js $scope.updateSubscriptionStatus = function () { Client.getSubscription(function (error, subscription) { if (error && error.statusCode === 412) return; // not yet registered if (error && error.statusCode === 402) return; // invalid appstore token if (error) return console.error(error); $scope.subscription = subscription; }); }; function refreshNotifications() { if (!Client.getUserInfo().isAtLeastAdmin) return; Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100 if (error) console.error(error); else $scope.notificationCount = results.length; }); } // update state of acknowledged notification $scope.notificationAcknowledged = function () { refreshNotifications(); }; function redirectOnMandatory2FA() { if (Client.getConfig().mandatory2FA) { if (Client.getUserInfo().twoFactorAuthenticationEnabled) return; // user already has 2fa if (Client.getUserInfo().source && $scope.config.external2FA) return; // 2fa is external $location.path('/profile').search({ setup2fa: true }); } } // Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force $scope.$on('$routeChangeStart', function (/* event */) { if ($scope.initialized) redirectOnMandatory2FA(); }); var gPlatformStatusNotification = null; function trackPlatformStatus() { Client.getPlatformStatus(function (error, result) { if (error) return console.error('Failed to get platform status.', error); // see box/src/platform.js if (result.message === 'Ready') { if (gPlatformStatusNotification) { gPlatformStatusNotification.kill(); gPlatformStatusNotification = null; } return; } if (!gPlatformStatusNotification) { var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false }; Notification.primary(options).then(function (result) { gPlatformStatusNotification = result; $timeout(trackPlatformStatus, 5000); }); } else { gPlatformStatusNotification.message = result.message; $timeout(trackPlatformStatus, 5000); } }); } // this loads the very first thing when accessing via IP or domain function init() { Client.getProvisionStatus(function (error, status) { if (error) return Client.initError(error, init); if (redirectIfNeeded(status, 'dashboard')) return; // we got redirected... // check version and force reload if needed if (!localStorage.version) { localStorage.version = status.version; } else if (localStorage.version !== status.version) { localStorage.version = status.version; window.location.reload(true); } console.log('Running dashboard version ', localStorage.version); // get user profile as the first thing. this populates the "scope" and affects subsequent API calls async.series([ Client.refreshProfile.bind(Client), Client.refreshConfig.bind(Client), Client.refreshAvailableLanguages.bind(Client), Client.refreshInstalledApps.bind(Client) ], function (error) { if (error) return Client.initError(error, init); // now mark the Client to be ready Client.setReady(); $scope.config = Client.getConfig(); if (Client.getUserInfo().hasBackgroundImage) { document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")'; document.getElementById('mainContentContainer').classList.add('has-background'); } $scope.initialized = true; redirectOnMandatory2FA(); $interval(refreshNotifications, 60 * 1000); refreshNotifications(); Client.getSubscription(function (error, subscription) { if (error && error.statusCode === 412) return; // not yet registered if (error && error.statusCode === 402) return; // invalid appstore token if (error) return console.error(error); $scope.subscription = subscription; // only track platform status if we are registered trackPlatformStatus(); }); }); }); } Client.onConfig(function (config) { if (config.cloudronName) { document.title = config.cloudronName; } }); init(); // setup all the dialog focus handling ['updateModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find('[autofocus]:first').focus(); }); }); }]);