<?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>
 */
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 $_iMinTime4Timeline = 60;
    
    /**
     * 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($iLast){
        if(!$iLast){
            return '';
        }
        $iAge=round((date('U')-$iLast)/60);
        return '<div class="accessandage">'
            . sprintf($this->t("request-time"), date("Y-m-d H:i:s")).'<br>'
            . sprintf($this->t("last-entry"), $iAge)
            .'</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(){
        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($aData=false){
        $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer;
        $sHtml=$this->_getCacheData($sTaskId);
        if($sHtml){
            return $sHtml;
        }
        $sHtml='';
        
        if(!$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(array(
                    $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(array($iLast, date("U", $aEntry['SCRIPTSTARTTIME'])));
            
            $aErrors=array();
            $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'
                        .((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'
                        .($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 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> &nbsp; ' . $iErrors.'</a> ' : '')
                        . ( $iOK ? '<a href="#" class="btn bg-success" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-ok')).'"><i class="fas fa-check"></i> &nbsp; ' . $iOK.'</a>' : '') 
                        . ($iErrors && $iOK ? ' ... '.$this->t('total').': <a href="#" class="btn bg-gray" onclick="'.$this->_filterDatatable($sIdTable, "").'"><i class="fas fa-th-large"></i> &nbsp; ' . 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(){
        $aData=array();
        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
     * 
     * @param array  $aData   result of $oCL->getServerLogs()
     * @return string
     */
    public function renderInstances(){
        if(count($this->_aInstances) < 2){
            return false;
        }
        $sReturn='';
        $sServer=isset($_SERVER['SERVER_NAME']) ? $_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 renderJoblist($aData=false){
        $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer;
        $sHtml=$this->_getCacheData($sTaskId);
        if($sHtml){
            return $sHtml;
        }
        $sHtml='';
        
        if(!$aData){
            $aData=$this->getServerJobHistory();
        }

        $sTblHead='';
        $iOK=0;
        $iErrors=0;
        $iLast=false;
        // job=dok-kvm-instances:host=kalium:start=1538358001:end=1538358001:exectime=0:ttl=60:rc=0
        foreach($aData as $aEntry){
            if(!$sTblHead){
                foreach(array(
                    $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(array($iLast, date("U", $aEntry['start'])));
            $sClass='message-'.($aEntry['rc']?'error':'ok');
            
            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'
                        .($aEntry['exectime']>$this->_iMinTime4Timeline ? ' class="message-warning"' : '' )
                    . '>'
                        .$aEntry['exectime'].'s'
                        .($aEntry['exectime']>100 ? ' ('.round($aEntry['exectime']/60).'min)' : '') 
                    . '</td>'
                    . '<td>'.$aEntry['ttl'].'</td>'
                    . '<td 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> &nbsp; ' . $iErrors.'</a> ' : '')
                . ( $iOK ? '<a href="#" class="btn bg-success" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-ok')).'"><i class="fas fa-check"></i> &nbsp; ' . $iOK.'</a>' : '') 
                . ($iErrors && $iOK ? ' ... '.$this->t('total').': <a href="#" class="btn bg-gray" onclick="'.$this->_filterDatatable($sIdTable, "").'"><i class="fas fa-th-large"></i> &nbsp; ' . 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 renderJoblistOfAllServers(){
        $aData=array();
        foreach (array_keys($this->getServers()) as $sServer){
            $this->setServer($sServer);
            $aData=array_merge($aData, $this->getServerJobHistory());
        }
        $this->setServer('ALL');
        return $this->renderJoblist($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($aData=false){
        $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer;
        $sHtml=$this->_getCacheData($sTaskId);
        if($sHtml){
            return $sHtml;
        }
        $sHtml='';
        static $iGraphCounter;
        if(!isset($iGraphCounter)){
            $iGraphCounter=0;
        }
        $iGraphCounter++;
        if(!$aData){
            $aData=$this->getServerJobHistory(false);
        }
        $sDivId='vis-timeline-'.$iGraphCounter;
        
        $aDataset=array();
        $iLast=false;
        $iEntry=0;
        foreach($aData as $aEntry){
            if($aEntry['exectime']>$this->_iMinTime4Timeline){
                $iEntry++;
                $iLast=max(array($iLast, date("U", $aEntry['start'])));
                $aDataset[]=array(
                    '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);
              
              // 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>'
            ;
    }
    
    public function renderJobGraphOfAllServers(){
        $aData=array();
        foreach (array_keys($this->getServers()) as $sServer){
            $this->setServer($sServer);
            $aData=array_merge($aData, $this->getServerJobHistory());
        }
        $this->setServer('ALL');
        return $this->renderJobGraph($aData);
    }
    
    /**
     * used in config/page-replacements.php
     * generate an array of javascript lang texts
     */
    public function renderJSLang(){
        $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"
        ;
    }
    
   /**
    * show a single log file
    * 
    * @param string $sLogfile  logfile; [server]/[filename.log]
    * @return string
    */
   public function renderLogfile($sLogfile){
        $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($sSelectedItem=false){
        $sHtml='';
        $iMaxItemsToShow=30;
        $sHtml.='<option value="ALL"'
                .($sSelectedItem===false || $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(array(count($this->getServers())+1 , $iMaxItemsToShow)) ).'"'
                    // . ' size="1"'
                    . ' onchange="setServer(this.value); return false;"'
                    . '>'.$sHtml.'</select>' 
                : false;
        return $sHtml;
    }
    
}