<?php require_once 'cronlog.class.php'; /** * ______________________________________________________________________ * * _____ _ _ _ * / __ \ (_) | | (_) * | / \/_ __ ___ _ __ _ ___ | |__ __ ___ _____ _____ _ __ * | | | '__/ _ \| '_ \| |/ _ \| '_ \ \ \ / / |/ _ \ \ /\ / / _ \ '__| * | \__/\ | | (_) | | | | | (_) | |_) | \ V /| | __/\ V V / __/ | * \____/_| \___/|_| |_| |\___/|_.__/ \_/ |_|\___| \_/\_/ \___|_| * _/ | * |__/ * ______________________________________________________________________ * * The cronjob viewer for centralized monitoring of cronjobs using * Axels cronrwapper (see <https://github.com/axelhahn/cronwrapper>). * * You can browse all servers to see * - the last status of all cronjobs * - results an execution times of running jonbs * - a timeline for all jobs running > 60s * * * Free software. Open Source. GNU GPL 3. * SOURCE: <https://git-repo.iml.unibe.ch/iml-open-source/cronlog-viewer> * * ______________________________________________________________________ * * The class cronlog-renderer contains visual methods to render html * @see cronlog.class.php * * @license GNU GPL 3.0 * @author Axel Hahn <axel.hahn@iml.unibe.ch> * * 2024-10-01 <axel.hahn@unibe.ch> added type declarations; update php docs */ class cronlogrenderer extends cronlog { /** * minimal length for execution time of a job to be rendered in the timeline; value is in seconds * @var integer */ protected int $_iMinTime4Timeline = 60; /** * Max count of entries in history table to prevent freezing of the screen * when displaying a lot of entries, eg all logs of 100+ servers * @var int */ protected int $_iHistoryLimit = 1000; /** * Show date of last data and last access; used in rendering methods to display it on top * @param integer $iLast unix timestamp of last log entry * @return string */ protected function _renderAccessAndAge(int $iLast): string { if (!$iLast) { return ''; } $iAge = round((date('U') - $iLast) / 60); $sAge = ($iAge > 60*24 ? '<span class="message-error">'.$iAge.'</span>' : $iAge); return '<div class="accessandage">' . sprintf($this->t("request-time"), date("Y-m-d H:i:s")) . '<br>' . sprintf($this->t("last-entry"), $sAge) /* .($iAge > 60*24 ? " <a href=\"?deleteserverlogs=$this->_sActiveServer\" class=\"btn btn-danger\">Delete server</a>" : '' ) */ . '</div>' ; } /** * Get onclick value to filter a datatable * @param string $sDatatable id of table * @param string $sFiltertext text to filter * @return string * */ protected function _filterDatatable($sDatatable, $sFiltertext) { return '$(\'#' . $sDatatable . '\').dataTable().fnFilter(\'' . $sFiltertext . '\'); return false;'; } /** * Helper function to be used in methods that render datatable tables * Get javascript code to be added in init options and set language specifi texts * @return string */ protected function _getDatatableLanguage(): string { return $this->t("dt-USE") ? ', "oLanguage":{ "sProcessing":"' . $this->t("dt-sProcessing") . '", "sLengthMenu":"' . $this->t("dt-sLengthMenu") . '", "sZeroRecords":"' . $this->t("dt-sZeroRecords") . '", "sInfo":"' . $this->t("dt-sInfo") . '", "sInfoEmpty":"' . $this->t("dt-sInfoEmpty") . '", "sInfoFiltered":"' . $this->t("dt-sInfoFiltered") . '", "sInfoPostFix":"' . $this->t("dt-sInfoPostFix") . '", "sSearch":"' . $this->t("dt-sSearch") . '", "sUrl":"' . $this->t("dt-sUrl") . '", "oPaginate":{ "sFirst":"' . $this->t("dt-sFirst") . '", "sPrevious":"' . $this->t("dt-sPrevious") . '", "sNext":"' . $this->t("dt-sNext") . '", "sLast":"' . $this->t("dt-sLast") . '" } }' : '' ; } /** * Get html code for a table with events of executed cronjobs * * @param array $aData result of $this->getServerLogs() * @return string */ public function renderCronlogs(array $aData = []): string { $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; $sHtml = $this->_getCacheData($sTaskId); if ($sHtml) { return $sHtml; } $sHtml = ''; if (!count($aData)) { $aData = array_merge($this->getRunningJobs(), $this->getServersLastLog()); } $sTblHead = ''; $iOK = 0; $iErrors = 0; $iLast = false; // Array ( [SCRIPTNAME] => apt-get update [SCRIPTTTL] => 1440 [SCRIPTSTARTTIME] => 2016-06-21 06:00:02, 1466481602 [SCRIPTLABEL] => apt-get [SCRIPTENDTIME] => 2016-06-21 06:00:49, 1466481649 [SCRIPTEXECTIME] => 47 s [SCRIPTRC] => 0 ) foreach ($aData as $sDtakey => $aEntry) { if (!$sTblHead) { foreach ([ $this->t("col-starting-time"), $this->t("col-label"), $this->t("col-server"), $this->t("col-duration"), $this->t("col-ttl"), $this->t("col-rc"), $this->t("col-expired"), $this->t("col-status"), ] as $sKey) { $sTblHead .= "<th>$sKey</th>"; } } // $sViewerUrl='viewer.php?host='.$aEntry['host'].'&job='.$aEntry['job']; // $sClass='message-'.($aEntry['SCRIPTRC']?'error':'ok'); $iLast = max([$iLast, date("U", $aEntry['SCRIPTSTARTTIME'])]); $aErrors = []; $iTtlUsed = max($aEntry['SCRIPTTTL'], $this->_iMinTtl); $iNextRun = $aEntry['SCRIPTSTARTTIME'] + ((int) $aEntry['SCRIPTTTL'] * 60); $iNextRunWarn = $aEntry['SCRIPTSTARTTIME'] + ((int) $iTtlUsed * 60); $iNextRunErr = $aEntry['SCRIPTSTARTTIME'] + (((int) $aEntry['SCRIPTTTL'] + (int) $this->_iExpiredJobsFailAfter) * 60); // ticket #5850 - check hostname vs. servername in logfile $sServerFromLogfile = preg_replace('/_.*/', '', basename($aEntry['logfile'])); if ($sServerFromLogfile != $aEntry['server']) { $aErrors[] = [ $this->t('error-host-in-log-differs-servername-label'), sprintf($this->t('error-host-in-log-differs-servername-description'), $sServerFromLogfile, $aEntry['server']), ]; } if (!strstr($sServerFromLogfile, ".")) { $aErrors[] = [ $this->t('error-no-fqdn-label'), sprintf($this->t('error-no-fqdn-description'), $sServerFromLogfile), ]; } if ($iNextRunErr < date("U")) { $aErrors[] = [ $this->t('error-expired-label'), $this->t('error-expired-description'), ]; } if ($aEntry['SCRIPTRC'] > 0) { $aErrors[] = [ sprintf($this->t('error-exitcode-label'), (int) $aEntry['SCRIPTRC']), $this->t('error-exitcode-description'), ]; } if (!$aEntry['SCRIPTLABEL']) { $aErrors[] = [ $this->t('error-no-label-label'), $this->t('error-no-label-description'), ]; } if (count($aErrors)) { $iErrors++; } else { $iOK++; } // human readable TTL value ... SCRIPTTTL is in [min] $sTtlHr = ''; if ((int) $aEntry['SCRIPTTTL'] > 60 * 24 * 3) { $sTtlHr .= '=' . round((int) $aEntry['SCRIPTTTL'] / 60 / 24) . 'd '; } else if ((int) $aEntry['SCRIPTTTL'] > 60 * 3) { $sTtlHr .= '=' . round((int) $aEntry['SCRIPTTTL'] / 60) . 'h'; } $sTtlHr = $sTtlHr ? '(' . $sTtlHr . ')' : ''; $sColStatus = ''; $bIsRunning = false; if ($aEntry['SCRIPTRC']) { if (count($aErrors)) { foreach ($aErrors as $aErr) { $sColStatus .= '<li><abbr title="' . $aErr[1] . '">' . $this->t('status-error') . ': ' . $aErr[0] . '</abbr></li>'; } $sColStatus = '<ul>' . $sColStatus . '</ul>'; } else { $sColStatus .= $this->t('status-ok'); } } else { $bIsRunning = true; $sColStatus .= $this->t('status-running'); } // execution time of time of currently running job $iExectime = $aEntry['SCRIPTEXECTIME'] ? (int) $aEntry['SCRIPTEXECTIME'] : date('U') - $aEntry['SCRIPTSTARTTIME'] ; // render table of last logfile per cron job $sHtml .= '<tr onclick="showFile(\'' . $aEntry['logfile'] . '\');" title="' . sprintf($this->t('row-click-show-logfile'), $aEntry['logfile']) . '"' . ($bIsRunning ? ' class="message-running"' : '') . '>' . '<td>' . date("Y-m-d H:i:s", $aEntry['SCRIPTSTARTTIME']) . '</td>' // . '<td>'.$aEntry['SCRIPTNAME'].'</td>' . '<td>' . $aEntry['SCRIPTLABEL'] . '</td>' . '<td>' . $aEntry['server'] . '</td>' . '<td align="right"' . ((int) $aEntry['SCRIPTEXECTIME'] > $this->_iMinTime4Timeline ? ' class="message-warning"' : '') . '>' . '<span style="display: none">' . str_pad((int) $aEntry['SCRIPTEXECTIME'], 6, '0', STR_PAD_LEFT) . '</span>' . $iExectime . 's' . ($iExectime > 100 ? ' (' . round($iExectime / 60) . 'min)' : '') . '</td>' . '<td align="right"' . ($aEntry['SCRIPTTTL'] < $this->_iMinTtl ? ' class="message-warning" title="(using minimal TTL = ' . $this->_iMinTtl . ' min)"' : '') . '>' . '<span style="display: none">' . str_pad((int) $aEntry['SCRIPTTTL'], 6, '0', STR_PAD_LEFT) . '</span>' . $aEntry['SCRIPTTTL'] . $sTtlHr . '</td>' . '<td align="right" class="' . ($aEntry['SCRIPTRC'] ? ($aEntry['SCRIPTRC'] > 0 ? 'message-error' : 'message-ok') : '') . '">' . ($aEntry['SCRIPTRC'] ? $aEntry['SCRIPTRC'] : '⏳') . '</td>' . '<td class="' . ($iNextRunWarn < date("U") ? ($iNextRunErr < date("U") ? 'message-error' : 'message-warning') : '') . '">' . date("Y-m-d H:i", $iNextRun) . '</td>' . '<td' . (count($aErrors) ? ' class="message-error" title=""' : '') . '>' . $sColStatus . '</td>' // .(count($aErrors) ? 'FEHLER' : 'OK').'</td>' // . '<td><button onclick="showFile(\''.$aEntry['logfile'].'\');">Ansehen</button></td>' . '</tr>' ; } $sIdTable = 'datatable1'; $sHtml = ' <!-- START ' . __METHOD__ . ' --> ' . '<h3>' . $this->t('logs-head') . '</h3>' . '<p class="hint">' . $this->t('logs-hint') . '</p>' . (count($aData) ? '<div>' . $this->_renderAccessAndAge($iLast) . ($iErrors ? '<a href="#" class="btn bg-danger" onclick="' . $this->_filterDatatable($sIdTable, $this->t('status-error')) . '"><i class="fas fa-exclamation-circle"></i> ' . $iErrors . '</a> ' : '') . ($iOK ? '<a href="#" class="btn bg-success" onclick="' . $this->_filterDatatable($sIdTable, $this->t('status-ok')) . '"><i class="fas fa-check"></i> ' . $iOK . '</a>' : '') . ($iErrors && $iOK ? ' ... ' . $this->t('total') . ': <a href="#" class="btn bg-gray" onclick="' . $this->_filterDatatable($sIdTable, "") . '"><i class="fas fa-th-large"></i> ' . count($aData) . '</a>' : '') . '</div><br>' . '<table id="' . $sIdTable . '" class="table-striped">' . '<thead><tr>' . $sTblHead . '</tr></thead>' . '<tbody>' . $sHtml . '</tbody>' . '</table>' . '<script>' . '$(document).ready( function () { $(\'#' . $sIdTable . '\').DataTable({ "retrieve": true, "bPaginate":false, "aaSorting":[[0,"desc"]] ' . $this->_getDatatableLanguage() . ' }); });' . '</script>' : '' ) // init datatable . ' <!-- ENDE ' . __METHOD__ . ' --> ' ; $this->_writeCacheData($sTaskId, $sHtml); return $sHtml; } /** * Get html code for a table with events of executed cronjobs for ALL servers * * @param array $aData result of $this->getServerLogs() * @return string */ public function renderCronlogsOfAllServers(): string { $aData = []; foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); $aData = array_merge($aData, $this->getRunningJobs(), $this->getServersLastLog()); } $this->setServer('ALL'); return $this->renderCronlogs($aData); } /** * Get html code for a switcher of multiple instances. * It returns an empty string if there is no / only one instance * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderInstances(): string { if (count($this->_aInstances) < 2) { return ''; } $sReturn = ''; $sServer = isset($_SERVER['SERVER_NAME']) ?? false; $sReturn .= '<li class="nav-item"><a class="nav-link nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a></li>' . '<li class="nav-item d-none d-sm-inline-block"><a href="#" class="nav-link">' . $this->t('instances') . ':</a></li>'; foreach ($this->_aInstances as $sInstance => $sUrl) { $sHost = parse_url($sUrl, PHP_URL_HOST); $sClass = ($sServer && $sServer == $sHost) ? 'active bg-gray' : ''; // $sReturn.='<a class="'.$sClass.'" href="'.$sUrl.'" title="'.$sUrl.'">'.$sInstance.'</a> '; $sReturn .= '<li class="nav-item d-none d-sm-inline-block ' . $sClass . '"><a href="' . $sUrl . '" class="nav-link">' . $sInstance . '</a></li>'; } return $sReturn; } /** * Get html code for a table with history of executed cronjobs * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderHistoryTable(array $aData = []): string { $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; $sHtml = $this->_getCacheData($sTaskId); if ($sHtml) { return $sHtml; } $sHtml = ''; if (!$aData) { $aData = $this->getServerJobHistory(); } // sort by starting time - especially before cutting the list $aTmp=[]; foreach($aData as $sKey => $aEntry) { $aTmp[$aEntry['start'].'__'.$aEntry['host'].'__'.$aEntry['job']]=$aEntry; } krsort($aTmp); $aData = array_values($aTmp); // render table $sTblHead = ''; $iOK = 0; $iErrors = 0; $iLast = false; $iCounter = 0; // job=dok-kvm-instances:host=kalium:start=1538358001:end=1538358001:exectime=0:ttl=60:rc=0 foreach ($aData as $aEntry) { $iCounter++; if($iCounter>$this->_iHistoryLimit) { break; } if (!$sTblHead) { foreach ([ $this->t("col-starting-time"), $this->t("col-label"), $this->t("col-server"), $this->t("col-duration"), $this->t("col-ttl"), $this->t("col-rc"), $this->t("col-status"), ] as $sKey) { $sTblHead .= '<th>' . $sKey . '</th>'; } } $iLast = max([$iLast, date("U", $aEntry['start'])]); if ($aEntry['rc']) { $iErrors++; } else { $iOK++; } $sHtml .= '<tr>' . '<td>' . date("Y-m-d H:i:s", $aEntry['start']) . '</td>' // . '<td>'.date("Y-m-d H:i:s", $aEntry['end']).'</td>' . '<td>' . $aEntry['job'] . '</td>' . '<td>' . $aEntry['host'] . '</td>' . '<td align="right"' . ($aEntry['exectime'] > $this->_iMinTime4Timeline ? ' class="message-warning"' : '') . '>' . $aEntry['exectime'] . 's' . ($aEntry['exectime'] > 100 ? ' (' . round($aEntry['exectime'] / 60) . 'min)' : '') . '</td>' . '<td align="right">' . $aEntry['ttl'] . '</td>' . '<td align="right" class="' . ($aEntry['rc'] > 0 ? 'message-error' : 'message-ok') . '">' . $aEntry['rc'] . '</td>' . '<td>' . ($aEntry['rc'] ? $this->t('status-error') : $this->t('status-ok')) . '</td>' . '</tr>' ; } $sIdTable = 'datatable2'; $sHtml = ' <!-- START ' . __METHOD__ . ' --> ' . '<h3>' . $this->t('history-head') . '</h3>' . '<p class="hint">' . $this->t('history-hint') . '</p>' . '<div>' . $this->_renderAccessAndAge($iLast) . ($iErrors ? '<a href="#" class="btn bg-danger" onclick="' . $this->_filterDatatable($sIdTable, $this->t('status-error')) . '"><i class="fas fa-exclamation-circle"></i> ' . $iErrors . '</a> ' : '') . ($iOK ? '<a href="#" class="btn bg-success" onclick="' . $this->_filterDatatable($sIdTable, $this->t('status-ok')) . '"><i class="fas fa-check"></i> ' . $iOK . '</a>' : '') . ($iErrors && $iOK ? ' ... ' . $this->t('total') . ': <a href="#" class="btn bg-gray" onclick="' . $this->_filterDatatable($sIdTable, "") . '"><i class="fas fa-th-large"></i> ' . count($aData) . '</a>' : '') . '</div>' . '<br>' . '<table id="' . $sIdTable . '">' . '<thead><tr>' . $sTblHead . '</tr></thead>' . '<tbody>' . $sHtml . '</tbody>' . '</table>' // init datatable . (count($aData) ? '<script>' . '$(document).ready( function () {$(\'#' . $sIdTable . '\').DataTable({ "retrieve": true, "aaSorting":[[0,"desc"]], "aLengthMenu":[[25,100,-1],[25,100,"---"]] ' . $this->_getDatatableLanguage() . ' });} );' . '</script>' : '' ) . ' <!-- ENDE ' . __METHOD__ . ' --> ' ; $this->_writeCacheData($sTaskId, $sHtml); return $sHtml; } /** * get html code for a joblist of the selected server * it uses the filter for hidden joblog entries (aHidelogs in config) * * @return string */ public function renderHistoryOfAllServers(): string { $aData = []; foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); $aData = array_merge($aData, $this->getServerJobHistory()); } $this->setServer('ALL'); return $this->renderHistoryTable($aData); } /** * get html code for a timeline with events of executed cronjobs * * TODO: * for rendering of several hosts ... see grouping in * http://visjs.org/examples/timeline/groups/groupsOrdering.html * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderJobGraph(array $aData = []): string { $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; $sHtml = $this->_getCacheData($sTaskId); if ($sHtml) { return $sHtml; } $sHtml = ''; static $iGraphCounter; if (!isset($iGraphCounter)) { $iGraphCounter = 0; } $iGraphCounter++; if (!count($aData)) { $aData = $this->getServerJobHistory(false); } $sDivId = 'vis-timeline-' . $iGraphCounter; $aDataset = []; $iLast = false; $iEntry = 0; foreach ($aData as $aEntry) { if ($aEntry['exectime'] > $this->_iMinTime4Timeline) { $iEntry++; $iLast = max([$iLast, date("U", $aEntry['start'])]); $aDataset[] = [ 'id' => $iEntry, /* 'start'=>(int)date("U", $aEntry['start']), 'end'=>(int)date("U", $aEntry['end']), * 'start'=>'Date'.date('Y-m-d\TH:i:s.000\Z', $aEntry['start']), 'end'=>'Date'.date('Y-m-d\TH:i:s.000\Z', $aEntry['end']), * 'start'=>date('Y-m-d\TH:i:s.000\Z', $aEntry['start']), 'end'=>date('Y-m-d\TH:i:s.000\Z', $aEntry['end']), * */ 'start' => (int) date("U", $aEntry['start']) * 1000, 'end' => (int) date("U", $aEntry['end']) * 1000, 'content' => $aEntry['job'] . '@' . $aEntry['host'], 'className' => 'timeline-result-' . ($aEntry['rc'] ? 'error' : 'ok'), 'title' => '<strong>' . $aEntry['job'] . '@' . $aEntry['host'] . '</strong><br>' . 'start: ' . date("Y-m-d H:i:s", $aEntry['start']) . '<br>' . 'end: ' . date("Y-m-d H:i:s", $aEntry['end']) . '<br>' . 'exectime: ' . $aEntry['exectime'] . 's' . ($aEntry['exectime'] > 100 ? ' (' . round($aEntry['exectime'] / 60) . 'min)' : '') . '<br>' . 'rc = ' . $aEntry['rc'] . '<br>' , ]; // if($iEntry>=265){break;} } } $sHtml .= ' <!-- START ' . __METHOD__ . ' --> ' . '<h3>' . $this->t('timeline-head') . '</h3>' . '<p class="hint">' . sprintf($this->t('timeline-hint'), $this->_iMinTime4Timeline) . '</p> <p>' . sprintf($this->t('graph-rendered-jobs'), $this->_iMinTime4Timeline) . ': <strong>' . count($aDataset) . '</strong> ' . '(' . $this->t('total') . ': ' . count($aData) . ')<br><br>' . (count($aDataset) ? $this->_renderAccessAndAge($iLast) : '') . '</p>' . (count($aDataset) ? '<div id="' . $sDivId . '"></div> <script type="text/javascript"> // DOM element where the Timeline will be attached var container = document.getElementById("' . $sDivId . '"); // Create a DataSet (allows two way data-binding) var items = new vis.DataSet(' . json_encode($aDataset) . '); // Configuration for the Timeline var options = { zoomMin: 1000 * 60 * 60, // an hour zoomMax: 1000 * 60 * 60 * 24 * 14 // 2 weeks }; // Create a Timeline var timeline = new vis.Timeline(container, items, options); // set focus to newest jobs // timeline.moveTo("'.date("Y-m-d H:i:s", time()-60*60*24*4).'"); // fix: some timelines do not properly work ... but I make them visible $(\'#' . $sDivId . ' .vis-timeline\').css(\'visibility\', \'visible\'); </script> ' : $this->t('graph-no-data') . '<br>' ) . ' <!-- ENDE ' . __METHOD__ . '--> '; $this->_writeCacheData($sTaskId, $sHtml); return '' . $sHtml // . strlen($sHtml).' byte - ' . '<pre>'.htmlentities($sHtml).'</pre><br>' ; } /** * Get html code for the job graph of all servers * @return string */ public function renderJobGraphOfAllServers(): string { $aData = []; foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); $aData = array_merge($aData, $this->getServerJobHistory()); } $this->setServer('ALL'); return $this->renderJobGraph($aData); } /** * generate an array of javascript lang texts * used in config/page-replacements.php * @return string */ public function renderJSLang(): string { $aReturn = []; foreach ($this->_aLang as $sKey => $sText) { if (preg_match('/^JS_/', $sKey)) { $aReturn[preg_replace('/^JS_/', '', $sKey)] = $sText; } } return "\n" . '// generated by ' . __METHOD__ . "\n" . 'var aLang=' . json_encode($aReturn, JSON_PRETTY_PRINT) . '; ' . "\n" ; } /** * Get html code to show a single log file with syntax highligt an marking the reeturn code * * @param string $sLogfile logfile; [server]/[filename.log] * @return string */ public function renderLogfile(string $sLogfile): string { $sHtml = '' . '<button style="position: fixed;" onclick="closeOverlay();" class="btn btn-default"><i class="fas fa-chevron-left"></i> ' . $this->t('back') . '</button><br><br>' . '<h3>' . $this->t('logfile') . ' ' . basename($sLogfile) . '</h3>' ; if (!$sLogfile) { return $sHtml . $this->t('error-nologfile'); } if (strstr($sLogfile, '..')) { return $sHtml . $this->t('error-dots-not-allowed'); } // $sMyFile=$this->_getServerlogDir().'/'.$sLogfile; $sMyFile = $this->_sDataDir . '/' . $sLogfile; if (!file_exists($sMyFile)) { return $sHtml . sprintf($this->t('error-logfile-not-found'), $sMyFile); } if ($fileHandle = fopen($sMyFile, "r")) { $sHtml .= '<div style="float: left;"><pre>'; while (($line = fgets($fileHandle)) !== false) { # do same stuff with the $line $bIsComment = strstr($line, 'REM '); if ($bIsComment) { $sHtml .= '<div class="log-rem">' . $line . '</div>'; } else { $sKey = trim(preg_replace('/=.*/', '', $line)); $sValue = preg_replace('/^([A-Z]*\=)/', '', $line); $sDivClass = ''; switch ($sKey) { case 'SCRIPTRC': $sDivClass = (int) $sValue === 0 ? 'message-ok' : 'message-error'; break; case 'JOBEXPIRE': $sDivClass = date('U') < (int) $sValue ? 'message-ok' : 'message-error'; break; } $sValue = preg_replace('/(rc=0)/', '<span class="message-ok">$1</span>', $sValue); $sValue = preg_replace('/(rc=[1-9][0-9]*)/', '<span class="message-error">$1</span>', $sValue); $sValue = preg_replace('/(rc=[1-9])/', '<span class="message-error">$1</span>', $sValue); // remove terminal color $sValue = preg_replace('/(\[[0-9]{1,3}m)/', '', $sValue); $sHtml .= '<div' . ($sDivClass ? ' class="' . $sDivClass . '"' : '') // . ' title="'.$sKey.'='.$sValue.'" ' . '><span class="log-var">' . $sKey . '</span>=<span class="log-value">' . $sValue . '</span></div>'; } } $sHtml .= '</pre></div>'; } return $sHtml; } /** * Get html code for a select box with all servers * @return string */ public function renderServerlist(string $sSelectedItem = ''): string { $sHtml = ''; $iMaxItemsToShow = 30; $sHtml .= '<option value="ALL"' . ($sSelectedItem === '' || $sSelectedItem === 'ALL' ? ' selected="selected"' : '') . '>[' . $this->t('ALL') . ' (' . count($this->getServers()) . ')]</option>'; foreach ($this->getServers() as $sServer => $aData) { $sHtml .= '<option value="' . $sServer . '"' . ($sSelectedItem === $sServer ? ' selected="selected"' : '') . '>' . $sServer . '</option>'; } $sHtml = $sHtml ? '' /* .'<input id="serverfiltertext" type="text" placeholder="filter server" value="" onchange="filterServers();" onkeypress="filterServers();" onkeyup="filterServers();" ><button onclick="$(\'#serverfiltertext\').val(\'\'); filterServers();">X</button><br><br>' */ . '<select' . ' size="' . (min([count($this->getServers()) + 1, $iMaxItemsToShow])) . '"' // . ' size="1"' . ' onchange="setServer(this.value); return false;"' . '>' . $sHtml . '</select>' : false; return $sHtml; } }