getPageJsCode(); } public function getPageJsCode($aOptions = array(), $bWrap = true) { $aStatusTitles = []; foreach(['ok', 'warn', 'fail', 'undef'] as $sStatus) $aStatusTitles[$sStatus] = _t('_sys_audit_title_' . $sStatus); $aOptions = array_merge($aOptions, array( 'sActionUrl' => BX_DOL_URL_STUDIO . 'dashboard.php', 'aStatusTitles' => $aStatusTitles // the status words the Updates rows get once the check is in )); return parent::getPageJsCode($aOptions, $bWrap); } public function getPageCode($sPage = '', $bWrap = true) { $sResult = parent::getPageCode($sPage, $bWrap); if($sResult === false) return false; $oPage = BxDolPage::getObjectInstance('sys_std_dashboard', BxDolStudioTemplate::getInstance()); return $sResult . $oPage->getCode(); } /** * The Version block's asynchronous part: the system update state and the count of apps with updates, as status rows. */ public function getPageCodeVersionAvailable() { // Row statuses are the status-row modifiers (ok / warn / undef); the audit class that names them isn't loaded here. // Latest Stable and Latest Beta each show the newest version on that channel: green when the site has it, orange when it is newer. $bUpgradeAvailable = false; $aRows = []; foreach(['stable', 'beta'] as $sChannel) { list($sVersion, $bNewer, $bUpgrade) = $this->getVersionChannelInfo($sChannel); if($sVersion) $aRows[$sChannel] = ['status' => $bNewer ? 'warn' : 'ok', 'value' => $sVersion]; else $aRows[$sChannel] = ['status' => 'undef', 'value' => _t('_adm_dbd_txt_ht_na')]; // the Update button in the block caption follows the channel the site is set to if($bUpgrade && $sChannel == (getParam('sys_upgrade_channel') == 'beta' ? 'beta' : 'stable')) $bUpgradeAvailable = true; } // The Apps row is named after the number of installed apps; its value is the number waiting for an update. $sApps = $this->getAppsCountText(); $mixedUpdates = BxDolStudioInstallerUtils::getInstance()->checkUpdates(); if(!is_array($mixedUpdates)) $aApps = ['status' => 'undef', 'name' => $sApps, 'value' => _t('_adm_dbd_txt_ht_na')]; else if(($iUpdates = count($mixedUpdates)) > 0) $aApps = ['status' => 'warn', 'name' => $sApps, 'value' => '' . _t('_adm_dbd_txt_ver_apps_updates', $iUpdates) . '']; else $aApps = ['status' => 'ok', 'name' => $sApps, 'value' => _t('_adm_dbd_txt_ver_up_to_date')]; return [ 'upgrade' => (int)$bUpgradeAvailable, 'stable' => $aRows['stable'], 'beta' => $aRows['beta'], 'apps' => $aApps ]; } public function serviceGetWidgetNotices() { $iResult = 0; //--- Check Version: a newer release on the channel the site follows list(, $bNewer) = $this->getVersionChannelInfo(getParam('sys_upgrade_channel') == 'beta' ? 'beta' : 'stable'); if($bNewer) $iResult += 1; //--- Check Host Requirements $oAudit = new BxDolStudioToolsAudit(); foreach($this->aItemsHTools as $sTitle => $sFunc) { $aResult = $oAudit->checkRequirements(BX_DOL_AUDIT_FAIL, $sFunc); if(!empty($aResult)) { $iResult += 1; break; } } return $iResult; } /** * "23 Apps": everything installed besides the system itself (apps, templates and languages). */ protected function getAppsCountText() { $iApps = 0; foreach(BxDolModuleQuery::getInstance()->getModules() as $aModule) if($aModule['type'] != 'system') $iApps++; return _t('_adm_dbd_txt_ver_apps_count', $iApps); } /** * Whether the site can receive automatic updates. They come with the UNA account's Pro and Max plans (https://unacms.com/start), which * the site joins with the marketplace key pair from Settings > System; swap the check for the account API's answer once it reports the plan. */ public function isAutoUpdateAccess() { return getParam('sys_oauth_key') != '' && getParam('sys_oauth_secret') != ''; } public function serviceGetBlockVersion() { $sJsObject = $this->getPageJsObject(); $aSysInfo = BxDolModuleQuery::getInstance()->getModuleByName('system'); // The update switches mirror the Settings > General options. $aToggles = [ ['name' => 'sys_autoupdate', 'title' => _t('_adm_dbd_txt_ver_auto'), 'icon' => 'refresh-cw', 'on' => getParam('sys_autoupdate') == 'on'], ['name' => 'sys_upgrade_channel', 'title' => _t('_adm_dbd_txt_ver_beta'), 'icon' => 'flask-conical', 'on' => getParam('sys_upgrade_channel') == 'beta'], ['name' => 'sys_autoupdate_force_modified_files', 'title' => _t('_adm_dbd_txt_ver_force'), 'icon' => 'zap', 'on' => getParam('sys_autoupdate_force_modified_files') == 'on'], ]; $aTmplVarsToggles = []; foreach($aToggles as $aToggle) $aTmplVarsToggles[] = [ 'js_object' => $sJsObject, 'name' => $aToggle['name'], 'title' => $aToggle['title'], 'icon' => $this->getChartIcon($aToggle['icon'])['svg'], 'state' => $aToggle['on'] ? 'on' : 'off', 'checked' => $aToggle['on'] ? 'true' : 'false', 'checked_attr' => $aToggle['on'] ? 'checked' : '' ]; // when the running version was applied: the last update, or the installation itself when it was never updated $iApplied = $aSysInfo['updated'] > 0 ? $aSysInfo['updated'] : $aSysInfo['date']; // the last status row: whether the site can receive automatic updates (see isAutoUpdateAccess) $bAccess = $this->isAutoUpdateAccess(); $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_versions.html', array( 'js_object' => $sJsObject, 'domain' => getParam('site_title'), 'version' => bx_get_ver(), 'version_time' => _t('_adm_dbd_txt_ver_applied', bx_time_js($iApplied, BX_FORMAT_DATE_TIME, true)), 'bx_repeat:toggles' => $aTmplVarsToggles, 'installed' => bx_time_js($aSysInfo['date'], BX_FORMAT_DATE, true), 'updated' => bx_time_js($iApplied, BX_FORMAT_DATE, true), 'icon_installed' => $this->getChartIcon('package-check')['svg'], 'icon_updated' => $this->getChartIcon('history')['svg'], 'icon_stable' => $this->getChartIcon('cloud-download')['svg'], 'icon_beta' => $this->getChartIcon('flask-conical')['svg'], 'icon_apps' => $this->getChartIcon('layout-grid')['svg'], 'apps' => $this->getAppsCountText(), 'checking' => _t('_adm_dbd_txt_ver_checking'), 'icon_access' => $this->getChartIcon('badge-check')['svg'], 'access_class' => $bAccess ? ' bx-dbd-htools-item-ok' : '', // the same bx_if twice in one row would be cut as one span by the template parser 'bx_if:access_active' => ['condition' => $bAccess, 'content' => []], 'bx_if:access_inactive' => ['condition' => !$bAccess, 'content' => [ 'access_url' => 'https://unacms.com/start' // a bx_if block only sees its own content ]] )); // The Update button sits in the block caption; dashboard.js shows it once the check finds an upgrade for the site's channel. return array( 'content' => $sContent, 'menu' => '' ); } /** * The system module's storage as Space rows per engine scope: the Storage app's files and images (an image's resized copies count * towards its size, not its file count), each linking to its tab of the Storage app, then everything else the system keeps (account * pictures, comment and editor images, wiki files, queues) as "System Media", linking to the Storage app itself. The system module has no icon of its own, and * resolving one would register a template location named 'system' and break page rendering, so these rows use Lucide glyphs. * @return array ['local' => rows, 'remote' => rows with objects on a remote engine] */ protected function getSystemStorageItems() { $aGroups = [ 'files' => ['objects' => ['sys_files'], 'counted' => ['sys_files'], 'label' => _t('_adm_dbd_txt_su_files'), 'icon' => 'file', 'url' => BX_DOL_URL_STUDIO . 'storages.php?page=files'], 'images' => ['objects' => ['sys_images', 'sys_images_resized'], 'counted' => ['sys_images'], 'label' => _t('_adm_dbd_txt_su_images'), 'icon' => 'image', 'url' => BX_DOL_URL_STUDIO . 'storages.php?page=images'], 'media' => ['objects' => [], 'counted' => [], 'label' => _t('_adm_dbd_txt_su_system_media'), 'icon' => 'hard-drive', 'url' => BX_DOL_URL_STUDIO . 'storages.php'], ]; $aTotals = []; foreach($aGroups as $sGroup => $aGroup) $aTotals[$sGroup] = ['local' => ['size' => 0, 'files' => 0, 'objects' => 0], 'remote' => ['size' => 0, 'files' => 0, 'objects' => 0]]; foreach($this->oDb->getStorageObjects('sys') as $aObject) { $sGroup = 'media'; foreach($aGroups as $sName => $aGroup) if(in_array($aObject['object'], $aGroup['objects'])) { $sGroup = $sName; break; } $sScope = $aObject['engine'] == 'Local' ? 'local' : 'remote'; $aTotals[$sGroup][$sScope]['size'] += (int)$aObject['current_size']; $aTotals[$sGroup][$sScope]['objects']++; if(in_array($aObject['object'], $aGroups[$sGroup]['counted'])) $aTotals[$sGroup][$sScope]['files'] += (int)$aObject['current_number']; } $aResult = ['local' => [], 'remote' => []]; foreach($aGroups as $sGroup => $aGroup) foreach(['local', 'remote'] as $sScope) { $aTotal = $aTotals[$sGroup][$sScope]; if($sScope == 'remote' && $aTotal['objects'] == 0) continue; $aResult[$sScope][] = [ 'label' => $aGroup['label'], 'value' => $aTotal['size'], 'icon' => $this->getChartIcon($aGroup['icon']), 'url' => $aGroup['url'], 'note' => !empty($aGroup['counted']) ? _t($aTotal['files'] == 1 ? '_adm_dbd_txt_su_file_count_1' : '_adm_dbd_txt_su_files_count', $aTotal['files']) : '' ]; } return $aResult; } /** * Users: a chart in the hero and the rows under it as its legend. The period tabs in the block caption (dbd_users_tabs.html, Total first * and selected) pick the time axis: the last 7 days, 30 days or year as a bar per day or month, or Total as the running total over all * time (a point per month, or per year once that spans more than two years). The rows pick the series: Accounts (added; first and selected), * Active users (last seen), a row per profile type (its profiles added, from the app's content table) and a row per permission level a * profile can hold (its current members by when they joined it; the pseudo levels are not rows), each showing its figure for the * chosen period; the hero shows the charted one's figure and title on the left and, on the right, how many of it are online now (a session * within the online window: every online account is an active user, a profile type's are those whose account is online). dashboard.js * (initUsersChart) draws from the data of every series over every axis, which all come with the block. */ public function serviceGetBlockUsers() { $sJsObject = $this->getPageJsObject(); $iNow = time(); // the axes: all time first (the default tab), a point per month since the first account, per year once that spans more than two years $iFirst = $this->oDb->getAccountsFirstAdded(); $iYears = $iFirst > 0 ? (int)date('Y', $iNow) - (int)date('Y', $iFirst) + 1 : 1; $iMonths = $iFirst > 0 ? ($iYears - 1) * 12 + (int)date('n', $iNow) - (int)date('n', $iFirst) + 1 : 1; $aAxes = [ 'all' => $iMonths > 24 ? ['year', $iYears, 'Y', 'Y'] : ['month', max($iMonths, 1), 'M', 'M Y'], '7d' => ['day', 7, 'D', 'j M Y'], '30d' => ['day', 30, 'j', 'j M Y'], '1y' => ['month', 12, 'M', 'M Y'], ]; $aPeriods = $aTmplVarsPeriods = []; foreach($aAxes as $sPeriod => $aAxis) { $aPeriods[$sPeriod] = [ 'title' => _t('_adm_dbd_txt_us_' . $sPeriod), 'label' => _t('_adm_dbd_txt_us_' . $sPeriod . '_label'), 'type' => $sPeriod == 'all' ? 'line' : 'bars' // the Total tab charts the running total of the buckets instead of the buckets ]; $aTmplVarsPeriods[] = [ 'js_object' => $sJsObject, 'name' => $sPeriod, 'title' => $aPeriods[$sPeriod]['title'], 'selected' => $sPeriod == 'all' ? 'true' : 'false', 'tabindex' => $sPeriod == 'all' ? '0' : '-1' ]; } // the series and where each is counted from $oDb = $this->oDb; $iOnline = $this->oDb->getAccountsOnlineCount(); $aSeries = [ 'accounts' => ['title' => _t('_adm_dbd_txt_us_accounts'), 'online' => $iOnline], 'active' => ['title' => _t('_adm_dbd_txt_us_active'), 'online' => $iOnline], ]; $aSources = [ 'accounts' => function($sFormat, $iSince) use($oDb) { return $oDb->getAccountsAddedBy($sFormat, $iSince); }, 'active' => function($sFormat, $iSince) use($oDb) { return $oDb->getAccountsSeenBy($sFormat, $iSince); }, ]; // one row per enabled profile app (the modules flagged with the profile subtype), at zero too, with its pending profiles as a note; // its series counts the app's content table by its added field, so an app without one gets a row that charts nothing $aCounts = []; foreach($this->oDb->getProfilesByType() as $aRow) $aCounts[$aRow['type']] = $aRow; $aTmplVarsProfiles = []; foreach(BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'modules_subtypes', 'value' => BX_DOL_MODULE_SUBTYPE_PROFILE]) as $aModule) { if(empty($aModule['enabled'])) continue; $sSeries = ''; $oModule = BxDolModule::getInstance($aModule['name']); $aCnf = $oModule && !empty($oModule->_oConfig->CNF) ? $oModule->_oConfig->CNF : []; if(!empty($aCnf['TABLE_ENTRIES']) && !empty($aCnf['FIELD_ADDED'])) { $sSeries = 'profiles_' . $aModule['name']; $sTable = $aCnf['TABLE_ENTRIES']; $sField = $aCnf['FIELD_ADDED']; $aSeries[$sSeries] = ['title' => $aModule['title'], 'online' => $this->oDb->getProfilesOnlineCount($aModule['name'])]; $aSources[$sSeries] = function($sFormat, $iSince) use($oDb, $sTable, $sField) { return $oDb->getEntriesAddedBy($sTable, $sField, $sFormat, $iSince); }; } $aRow = $aCounts[$aModule['name']] ?? ['total' => 0, 'active' => 0]; $iPending = (int)$aRow['total'] - (int)$aRow['active']; $aTmplVarsProfiles[] = [ 'js_object' => $sJsObject, 'series' => $sSeries, 'icon_url' => BxDolStudioUtils::getModuleIcon($aModule), 'name' => $aModule['title'], 'bx_if:show_note' => ['condition' => $iPending > 0, 'content' => ['note' => _t('_adm_dbd_txt_us_pending', $iPending)]], 'value' => number_format((int)$aRow['total']) ]; } // one row per permission level a profile can hold (BxDolStudioDashboardQuery::getLevels), with its current members; its series is when // they joined it, and its icon the level's own (a Lucide name), the shield when that is missing $aLevelCounts = $this->oDb->getLevelsMembersCount(); $aTmplVarsLevels = []; foreach($this->oDb->getLevels() as $aLevel) { $iLevel = (int)$aLevel['ID']; $sSeries = 'level_' . $iLevel; $sTitle = _t($aLevel['Name']); $aSeries[$sSeries] = ['title' => $sTitle, 'online' => $this->oDb->getLevelOnlineCount($iLevel)]; $aSources[$sSeries] = function($sFormat, $iSince) use($oDb, $iLevel) { return $oDb->getLevelMembersSinceBy($iLevel, $sFormat, $iSince); }; $sIcon = !empty($aLevel['Icon']) ? $this->getChartIcon($aLevel['Icon'])['svg'] : ''; $aTmplVarsLevels[] = [ 'js_object' => $sJsObject, 'series' => $sSeries, 'icon' => $sIcon ?: $this->getChartIcon('shield')['svg'], 'name' => $sTitle, 'value' => number_format((int)($aLevelCounts[$iLevel] ?? 0)) ]; } // every series over every axis; the Total axis carries the running total foreach($aSeries as $sSeries => &$aOne) { $aOne['periods'] = []; foreach($aAxes as $sPeriod => $aAxis) { $aItems = $this->getUsersChartBuckets($aAxis[0], $aAxis[1], $aAxis[2], $aAxis[3], $aSources[$sSeries]); if($sPeriod == 'all') { $iRunning = 0; foreach($aItems as &$aItem) { $iRunning += $aItem['count']; $aItem['total'] = $iRunning; } unset($aItem); } $aOne['periods'][$sPeriod] = $aItems; } } unset($aOne); $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_users.html', [ 'js_object' => $sJsObject, 'icon_accounts' => $this->getChartIcon('at-sign')['svg'], 'icon_active' => $this->getChartIcon('users')['svg'], 'accounts' => number_format($this->oDb->getAccountsCount()), 'active' => number_format(array_sum(array_column($aSeries['active']['periods']['all'], 'count'))), // dashboard.js keeps every row's figure on the chosen period 'bx_if:show_profiles' => ['condition' => !empty($aTmplVarsProfiles), 'content' => ['bx_repeat:profiles' => $aTmplVarsProfiles]], 'bx_if:show_levels' => ['condition' => !empty($aTmplVarsLevels), 'content' => ['bx_repeat:levels' => $aTmplVarsLevels]], 'chart_data' => json_encode([ 'period' => 'all', 'series_default' => 'accounts', 'periods' => $aPeriods, 'series' => $aSeries, 'txt_peak' => _t('_adm_dbd_txt_us_peak', '{0}') // the chart's accessible name names the peak bucket's count ]) ]); // the period switcher sits in the block caption, like the Server block's tabs return [ 'content' => $sContent, 'menu' => BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_users_tabs.html', ['js_object' => $sJsObject, 'bx_repeat:periods' => $aTmplVarsPeriods]) ]; } /** * A series' buckets over an axis: the last $iCount days, months or years up to now, each with the short label under its bar, its full * name for the tooltip and what $fCounts (a MySQL date format and the axis start, giving bucket => count) counted in it. */ protected function getUsersChartBuckets($sUnit, $iCount, $sLabel, $sTitle, $fCounts) { $aFormats = ['day' => ['%Y-%m-%d', 'Y-m-d'], 'month' => ['%Y-%m', 'Y-m'], 'year' => ['%Y', 'Y']]; list($sSqlFormat, $sPhpFormat) = $aFormats[$sUnit]; $iNow = time(); $aBuckets = []; for($i = $iCount - 1; $i >= 0; $i--) { switch($sUnit) { case 'month': $iTime = strtotime(date('Y-m-01', $iNow) . ' -' . $i . ' months'); break; case 'year': $iTime = mktime(0, 0, 0, 1, 1, (int)date('Y', $iNow) - $i); break; default: $iTime = strtotime(date('Y-m-d', $iNow) . ' -' . $i . ' days'); } $aBuckets[date($sPhpFormat, $iTime)] = ['label' => date($sLabel, $iTime), 'title' => date($sTitle, $iTime), 'count' => 0]; } $sFirst = array_key_first($aBuckets); $iSince = $sUnit == 'day' ? strtotime($sFirst . ' 00:00:00') : ($sUnit == 'month' ? strtotime($sFirst . '-01 00:00:00') : mktime(0, 0, 0, 1, 1, (int)$sFirst)); foreach($fCounts($sSqlFormat, $iSince) as $sBucket => $iAdded) if(isset($aBuckets[$sBucket])) $aBuckets[$sBucket]['count'] = (int)$iAdded; return array_values($aBuckets); } public function serviceGetBlockSpace($bDynamic = false) { $bInclideScriptSpace = false; //Use dynamic loading by default if this setting is enabled. $sJsObject = $this->getPageJsObject(); $aChartData = array(); if(!$bDynamic) { // Two charts and two lists: what lives on this server (the database, the Storage app's files and images, the rest of the system's // media, then every installed module's local storage, at zero too) and what lives on remote storage (whichever of those have // objects on a remote engine); when no remote storage is in use the second chart and list are a "not connected" placeholder. // Rows link to the page that manages them; the Storage app's rows also count their files. $aLocal = array( array('label' => _t('_adm_dbd_txt_su_database'), 'value' => $this->getDbSize(), 'icon' => $this->getChartIcon('database')), ); $aRemote = array(); if($bInclideScriptSpace) { $iSizeDiskTotal = $this->getFolderSize(BX_DIRECTORY_PATH_ROOT); $iSizeDiskMedia = $this->getFolderSize(BX_DIRECTORY_STORAGE); $aLocal[] = array('label' => _t('_adm_dbd_txt_su_system'), 'value' => $iSizeDiskTotal - $iSizeDiskMedia, 'icon' => $this->getChartIcon('hard-drive')); } $aSystem = $this->getSystemStorageItems(); $aLocal = array_merge($aLocal, $aSystem['local']); $aRemote = array_merge($aRemote, $aSystem['remote']); $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'all')); foreach($aModules as $aModule) { if($aModule['name'] == 'system') continue; $aIcon = $this->getChartIcon(BxDolStudioUtils::getModuleIcon($aModule), true); $sUrl = BX_DOL_URL_STUDIO . 'module.php?name=' . $aModule['name']; $aSizes = $this->oDb->getModuleStorageSizes($aModule['name']); $aLocal[] = array('label' => $aModule['title'], 'value' => $aSizes['local']['size'], 'icon' => $aIcon, 'url' => $sUrl); if($aSizes['remote']['objects'] > 0) $aRemote[] = array('label' => $aModule['title'], 'value' => $aSizes['remote']['size'], 'icon' => $aIcon, 'url' => $sUrl); } $fDiskTotal = @disk_total_space(BX_DIRECTORY_PATH_ROOT); $aChartLocal = $this->getChartData($this->getChartItems($aLocal), array_sum(array_column($aLocal, 'value')), $fDiskTotal !== false ? (float)$fDiskTotal : 0); $aChartLocal['label'] = bx_process_output(_t('_adm_dbd_txt_su_local')); $sRemote = bx_process_output(_t('_adm_dbd_txt_su_remote')); if($this->oDb->isRemoteStorageUsed()) { $aChartRemote = $this->getChartData($this->getChartItems($aRemote), array_sum(array_column($aRemote, 'value'))); $aChartRemote['label'] = $sRemote; $aRowsRemote = $aChartRemote['items']; } else { $sOff = bx_process_output(_t('_adm_dbd_txt_su_remote_off')); $aChartRemote = array('label' => $sRemote, 'items' => array(), 'used' => $sOff, 'total' => '', 'used_bytes' => 0, 'total_bytes' => 0); $aRowsRemote = array(array('name' => $sRemote, 'bytes' => 0, 'size' => $sOff, 'icon' => $this->getChartIcon('cloud-off'), 'placeholder' => true)); } $aChartData = array( 'charts' => array($aChartLocal, $aChartRemote), 'groups' => array(array('items' => $aChartLocal['items']), array('items' => $aRowsRemote)), ); } $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_space.html', array( 'bx_if:show_content' => array( 'condition' => !$bDynamic, 'content' => array( 'js_object' => $sJsObject, 'chart_data' => $this->getChartJson($aChartData) ) ), 'bx_if:show_loader' => array( 'condition' => $bDynamic, 'content' => array( 'js_object' => $sJsObject, ) ) )); return array('content' => $sContent); } public function serviceGetBlockHostTools($bDynamic = false) { $sJsObject = $this->getPageJsObject(); $oTemplate = BxDolStudioTemplate::getInstance(); // the links in the audit report (?action=audit_send_test_email / phpinfo) come back to this page: answered before anything is rendered (new BxDolStudioToolsAudit())->processRequest(); $aTmplVarsGauges = $aTmplVarsItems = []; if($bDynamic) { // Overview gauges: a donut per metric, its arc a share of the ring's circumference (2 * pi * 40); the ring shows the // metric's figure with its unit, when it has one, small underneath. $fRing = 251.327; foreach($this->getServerMetrics() as $sMetric => $aMetric) { $sNa = _t('_adm_dbd_txt_ht_na'); $aTmplVarsGauges[] = [ 'name' => $sMetric, 'class_add' => $aMetric !== null && $aMetric['percent'] >= 90 ? ' bx-dbd-gauge-high' : '', 'title' => _t('_adm_dbd_txt_ht_' . $sMetric), 'value' => $aMetric !== null ? $aMetric['value'] : $sNa, 'label' => $aMetric !== null ? trim($aMetric['value'] . ' ' . $aMetric['unit']) . ($aMetric['percent'] >= 90 ? ', ' . _t('_adm_dbd_txt_ht_high') : '') : $sNa, // the red arc alone would not tell a screen reader 'bx_if:show_unit' => [ 'condition' => $aMetric !== null && $aMetric['unit'] !== '', 'content' => ['unit' => $aMetric !== null ? $aMetric['unit'] : ''] ], 'dash' => $aMetric !== null ? round($aMetric['percent'] / 100 * $fRing, 2) : 0, 'ring' => $fRing ]; } // Status rows: a row per section of the last audit with its headline (a version) or its outcome, and the issue count when it has issues. foreach($this->getAuditReport()['sections'] as $aSection) { $aSummary = $this->getAuditSectionSummary($aSection); $aTmplVarsItems[] = [ 'status' => $aSummary['status'], 'status_title' => _t('_sys_audit_title_' . $aSummary['status']), // read out in place of the dot's colour 'icon' => $this->getChartIcon($aSection['icon'])['svg'], 'title' => $aSection['title'], 'value' => $aSummary['value'], 'bx_if:show_desc' => [ 'condition' => $aSummary['desc'] !== '', 'content' => ['desc' => $aSummary['desc']] ] ]; } } $sContent = $oTemplate->parseHtmlByName('dbd_htools.html', [ 'js_object' => $sJsObject, 'bx_if:show_content' => [ 'condition' => $bDynamic, 'content' => [ 'bx_repeat:gauges' => $aTmplVarsGauges, 'bx_repeat:items' => $aTmplVarsItems, ] ], 'bx_if:show_loader' => [ 'condition' => !$bDynamic, 'content' => [ 'js_object' => $sJsObject, ] ] ]); if($bDynamic) return ['content' => $sContent]; // The tab switcher sits in the block caption; the panels it drives arrive with the Overview. return [ 'content' => $sContent, 'menu' => $oTemplate->parseHtmlByName('dbd_htools_tabs.html', ['js_object' => $sJsObject]) ]; } /** * Server block, Audit tab: a hero with when the last audit ran, a bar of its outcomes and the button to run a new one, * then the sections as an accordion of status rows, and the note on what needs a person. */ public function getBlockHostToolsAudit() { $oAudit = new BxDolStudioToolsAudit(); $aReport = $this->getAuditReport(); $sJsObject = $this->getPageJsObject(); $sChevron = $this->getChartIcon('chevron-down')['svg']; $aTotals = ['ok' => 0, 'warn' => 0, 'fail' => 0, 'undef' => 0]; $aTmplVarsSections = []; foreach($aReport['sections'] as $aSection) { $aSummary = $this->getAuditSectionSummary($aSection); foreach($aSummary['counts'] as $sType => $iCount) $aTotals[$sType] += $iCount; $aTmplVarsRows = []; foreach($aSection['rows'] as $aRow) $aTmplVarsRows[] = $this->getHostToolsRowVars($aRow, $oAudit); $aTmplVarsSections[] = [ 'js_object' => $sJsObject, 'name' => $aSection['name'], 'status' => $aSummary['status'], 'icon' => $this->getChartIcon($aSection['icon'])['svg'], 'icon_chevron' => $sChevron, 'title' => $aSection['title'], 'value' => $aSummary['value'], 'bx_repeat:rows' => $aTmplVarsRows ]; } // The bar: a segment per outcome, passed first; the same counts are written out under the button. $iTotal = max(1, array_sum($aTotals)); $aTmplVarsSegments = $aSummary = []; foreach(['ok' => '_adm_dbd_txt_ht_passed', 'warn' => '_adm_dbd_txt_ht_warnings', 'fail' => '_adm_dbd_txt_ht_failed', 'undef' => '_adm_dbd_txt_ht_unknown'] as $sType => $sKey) { if($aTotals[$sType] == 0) continue; $sLabel = _t($sType == 'warn' && $aTotals[$sType] == 1 ? '_adm_dbd_txt_ht_warning' : $sKey, $aTotals[$sType]); $aTmplVarsSegments[] = ['type' => $sType, 'share' => round($aTotals[$sType] / $iTotal * 100, 2), 'label' => bx_html_attribute($sLabel)]; $aSummary[] = $sLabel; } ob_start(); $oAudit->generateJs(); $sJs = ob_get_clean(); return $sJs . BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_htools_audit.html', [ 'js_object' => $sJsObject, 'time' => bx_time_js($aReport['time'], BX_FORMAT_DATE_TIME), 'bar_label' => bx_html_attribute(implode(', ', $aSummary)), 'bx_repeat:segments' => $aTmplVarsSegments, 'summary' => implode(' · ', $aSummary), 'bx_repeat:sections' => $aTmplVarsSections ]); } /** * A section's outcome for the Status tab and the audit accordion: its worst row status (unknown rows only count when * there is nothing else), its headline (a version) or its outcome as the value, and, under a headline that stands for * several rows, the issue count as the note. * @return array status ('ok', 'warn', 'fail', 'undef'), value, desc, counts (rows per status) */ protected function getAuditSectionSummary($aSection) { $aCounts = ['ok' => 0, 'warn' => 0, 'fail' => 0, 'undef' => 0]; foreach($aSection['rows'] as $aRow) if(isset($aCounts[$aRow['type']])) $aCounts[$aRow['type']]++; $sStatus = 'undef'; foreach(['fail', 'warn', 'ok'] as $sType) if($aCounts[$sType] > 0) { $sStatus = $sType; break; } $aIssues = []; if($aCounts['fail'] > 0) $aIssues[] = _t('_adm_dbd_txt_ht_failed', $aCounts['fail']); if($aCounts['warn'] > 0) $aIssues[] = _t($aCounts['warn'] == 1 ? '_adm_dbd_txt_ht_warning' : '_adm_dbd_txt_ht_warnings', $aCounts['warn']); $sIssues = implode(', ', $aIssues); $bHeadline = $aSection['value'] !== ''; return [ 'status' => $sStatus, 'value' => $bHeadline ? $aSection['value'] : ($sIssues !== '' ? $sIssues : _t($sStatus == 'ok' ? '_sys_audit_title_ok' : '_sys_audit_title_undef')), 'desc' => $bHeadline && count($aSection['rows']) > 1 ? $sIssues : '', 'counts' => $aCounts ]; } /** * One audit row as status-row template variables; a row without a value shows its status word instead. */ protected function getHostToolsRowVars($aRow, $oAudit) { return [ 'status' => $aRow['type'], 'title' => $aRow['name'], 'value' => $aRow['value'] !== '' ? $aRow['value'] : $oAudit->typeToTitle($aRow['type']), 'bx_if:show_desc' => [ 'condition' => $aRow['msg'] !== '', 'content' => ['desc' => $aRow['msg']] ] ]; } public function serviceGetBlockCache() { $sJsObject = $this->getPageJsObject(); $sChartData = $this->getCacheChartData(); $bChartData = $sChartData !== false; $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_cache.html', [ 'bx_if:show_chart' => [ 'condition' => $bChartData, 'content' => [ 'js_object' => $sJsObject, 'chart_data' => $sChartData, ] ], 'bx_if:show_empty' => [ 'condition' => !$bChartData, 'content' => [ 'message' => MsgBox(_t('_adm_dbd_msg_c_all_disabled')) ] ], ]); return ['content' => $sContent]; } public function serviceGetBlockQueues() { $sChartData = $this->getQueuesChartData(); if($sChartData === false) return ['content' => '']; // "Clear all queues" is the hero's button (chart data), each row that can be emptied carries its own clear button. return ['content' => BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_queues.html', [ 'js_object' => $this->getPageJsObject(), 'chart_data' => $sChartData, ])]; } /** * @return array [latest version on the channel or '' when the check failed, whether it is newer than the installed one, whether an upgrade patch can be run] */ private function getVersionChannelInfo($sChannel) { $oUpgrader = bx_instance('BxDolUpgrader'); $aUpdateInfo = $oUpgrader->getVersionUpdateInfo($sChannel); if(!is_array($aUpdateInfo) || empty($aUpdateInfo['latest_version'])) return ['', false, false]; return [$aUpdateInfo['latest_version'], $oUpgrader->isNewVersionAvailable($aUpdateInfo), $oUpgrader->isUpgradeAvailable($aUpdateInfo)]; } } /** @} */