diff --git a/classes/cronlog-renderer.class.php b/classes/cronlog-renderer.class.php
index 165b9d0935b89f9f1d1e9be689aa64eb6083aa65..dbaebe8edc457e3e5a46041e426191be3b6bf2ef 100644
--- a/classes/cronlog-renderer.class.php
+++ b/classes/cronlog-renderer.class.php
@@ -14,8 +14,9 @@ require_once 'cronlog.class.php';
  * @author hahn
  */
 class cronlogrenderer extends cronlog{
-//put your code here
+
     
+    protected $_iMinTime4Timeline = 60;
     
     /**
      * get html code for a table with events of executed cronjobs
@@ -23,18 +24,157 @@ class cronlogrenderer extends cronlog{
      * @param array  $aData   result of $oCL->getServerLogs()
      * @return string
      */
-    public function renderJoblist($aData){
+    public function renderCronlogs($aData=false){
+        $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer;
+        $sHtml=$this->_getCacheData($sTaskId);
+        if($sHtml){
+            return $sHtml;
+        }
         $sHtml='';
         
+        if(!$aData){
+            $aData=$this->getServersLastLog();
+        }
+        // $sHtml='DEBUG: <pre>'.print_r($aData, 1).'</pre>';
+
         $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('SCRIPTSTARTTIME', 'SCRIPTLABEL', 'Server', 'SCRIPTEXECTIME', 'SCRIPTTTL', 'SCRIPTRC', 'Kommentar', 'Aktionen') as $sKey){
+                    $sTblHead.='<th>'.$sKey.'</th>';
+                }
+                $iLast=date("U", $aEntry['SCRIPTSTARTTIME']);
+            }
+            // $sViewerUrl='viewer.php?host='.$aEntry['host'].'&job='.$aEntry['job'];
+            // $sClass='message-'.($aEntry['SCRIPTRC']?'error':'ok');
+            
+            $aErrors=array();
+            $iNextRun=$aEntry['SCRIPTSTARTTIME']+($aEntry['SCRIPTTTL']*60);
+            
+            if($iNextRun < date("U")){
+                $aErrors[]='outdated';
+            }
+            if($aEntry['SCRIPTRC']>0){
+                $aErrors[]='exitcode is <>0';
+            }
+            if(!$aEntry['SCRIPTLABEL']){
+                $aErrors[]='no label?';
+            }
+            
+            
+            if(count($aErrors)){
+                $iErrors++;
+                $sClass='message-error';
+            } else {
+                $iOK++;
+                $sClass='message-ok';
+            }
+            
+            $sHtml.='<tr class="'.$sClass.'" onclick="showFile(\''.$aEntry['logfile'].'\');">'
+                    . '<td>'.date("Y-m-d H:i:s", $aEntry['SCRIPTSTARTTIME']).'</td>'
+                    // . '<td>'.$aEntry['SCRIPTNAME'].'</td>'
+                    . '<td>'.$aEntry['SCRIPTLABEL'].'</td>'
+                    . '<td>'.$aEntry['server'].'</td>'
+                    . '<td>'
+                        .$aEntry['SCRIPTEXECTIME'].'s'
+                        .($aEntry['SCRIPTEXECTIME']>100 ? ' ('.round($aEntry['SCRIPTEXECTIME']/60).'min)' : '') 
+                        .'</td>'
+                    . '<td>'.$aEntry['SCRIPTTTL'].'</td>'
+                    . '<td>'.$aEntry['SCRIPTRC'].'</td>'
+                    . '<td>'.(count($aErrors) ? 'FEHLER:<br>*'.implode('<br>*', $aErrors) : 'OK').'</td>'
+                    . '<td><button onclick="showFile(\''.$aEntry['logfile'].'\');">Ansehen</button></td>'
+                    . '</tr>'
+                    ;
+        }
+        $iAge=round((date('U')-$iLast)/60);
+        $sIdTable='datatable1';
+        $sHtml='
+            <!-- START '.__METHOD__.' -->
+            '
+        
+            . '<h3>Letztes Logfile pro Job</h3>'
+                . '<div>'
+                . 'Abruf: '.date("Y-m-d H:i:s").' min<br>'
+                . 'letzter Eintrag: vor '.$iAge.' min<br><br>'
+                . 'gesamt: <strong>' . count($aData).'</strong>'
+                . ($iErrors ? ' (Fehler: <strong>' . $iErrors.'</strong>... OK: <strong>' . $iOK.'</strong>)' : '')
+                . '<br><br>'
+                . '</div>'
+            . '<table id="'.$sIdTable.'">'
+            . '<thead><tr>'.$sTblHead.'</tr></thead>'
+            . '<tbody>'
+                .$sHtml
+            .'</tbody>'
+            . '</table>'
+
+            // init datatable
+            . '<script>'
+            . '$(document).ready( function () {$(\'#'.$sIdTable.'\').DataTable({"bPaginate":false, "aaSorting":[[0,"desc"]]});} );'
+            . '</script>'
+
+            . '
+            <!-- ENDE '.__METHOD__.' -->
+            '
+            ;
+        $this->_writeCacheData($sTaskId, $sHtml);
+        return $sHtml;
+    }
+   
+    public function renderCronlogsOfAllServers(){
+        $aData=array();
+        foreach (array_keys($this->getServers()) as $sServer){
+            $this->setServer($sServer);
+            $aData=array_merge($aData, $this->getServersLastLog());
+        }
+        $this->setServer('ALL');
+        // echo '<pre>'.print_r($aData, 1).'</pre>';
+        return $this->renderCronlogs($aData);
+    }
+    
+    /**
+     * get html code for a table with events 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('start', 'end', 'job', 'host', 'exectime', 'ttl', 'rc') as $sKey){
                     $sTblHead.='<th>'.$sKey.'</th>';
                 }
+                $iLast=date("U", $aEntry['start']);
+            }
+            // $sViewerUrl='viewer.php?host='.$aEntry['host'].'&job='.$aEntry['job'];
+            $sClass='message-'.($aEntry['rc']?'error':'ok');
+            
+            if($aEntry['rc']){
+                $iErrors++;
+            } else {
+                $iOK++;
             }
-            $sHtml.='<tr>'
+            
+            $sHtml.='<tr class="'.$sClass.'">'
                     . '<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>'
@@ -48,12 +188,38 @@ class cronlogrenderer extends cronlog{
                     . '</tr>'
                     ;
         }
-        return '<table>'
+        $iAge=round((date('U')-$iLast)/60);
+        $sIdTable='datatable2';
+        $sHtml='
+            <!-- START '.__METHOD__.' -->
+            '
+        
+            . '<h3>History</h3>'
+                . '<div>'
+                . 'Abruf: '.date("Y-m-d H:i:s").' min<br>'
+                . 'letzter Eintrag: vor '.$iAge.' min<br><br>'
+                . 'gesamt: <strong>' . count($aData).'</strong>'
+                . ($iErrors ? ' (Fehler: <strong>' . $iErrors.'</strong>... OK: <strong>' . $iOK.'</strong>)' : '')
+                . '<br><br>'
+                . '</div>'
+            . '<table id="'.$sIdTable.'">'
             . '<thead><tr>'.$sTblHead.'</tr></thead>'
             . '<tbody>'
                 .$sHtml
             .'</tbody>'
-            . '</table>';
+            . '</table>'
+
+            // init datatable
+            . '<script>'
+            . '$(document).ready( function () {$(\'#'.$sIdTable.'\').DataTable({"aaSorting":[[0,"desc"]]});} );'
+            . '</script>'
+
+            . '
+            <!-- ENDE '.__METHOD__.' -->
+            '
+            ;
+        $this->_writeCacheData($sTaskId, $sHtml);
+        return $sHtml;
     }
     /**
      * get html code for a timeline with events of executed cronjobs
@@ -65,42 +231,52 @@ class cronlogrenderer extends cronlog{
      * @param array  $aData   result of $oCL->getServerLogs()
      * @return string
      */
-    public function renderJobGraph($aData){
+    public function renderJobGraph($aData=false){
         $sHtml='';
         static $iGraphCounter;
         if(!isset($iGraphCounter)){
             $iGraphCounter=0;
         }
         $iGraphCounter++;
-        
+        if(!$aData){
+            $aData=$this->getServerJobHistory();
+        }
         $sDivId='vis-timeline-'.$iGraphCounter;
         
         $aDataset=array();
         $iEntry=0;
         foreach($aData as $aEntry){
             $iEntry++;
-            $aDataset[]=array(
-                'id'=>$iEntry,
-                'start'=>date("Y-m-d H:i:s", $aEntry['start']),
-                'end'=>date("Y-m-d H:i:s", $aEntry['end']),
-                '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($aEntry['exectime']>$this->_iMinTime4Timeline){
+                $aDataset[]=array(
+                    'id'=>$iEntry,
+                    'start'=>date("Y-m-d H:i:s", $aEntry['start']),
+                    'end'=>date("Y-m-d H:i:s", $aEntry['end']),
+                    '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>'
+                        ,
+                );
+            }
         }
         
         $sHtml.='
         
         <!-- START '.__METHOD__.' -->
 
+        <h3>Graph mit Timeline</h3>
+
+        <p>
+            Jobs ges.: <strong>'.count($aData).'</strong> ... davon mit mehr als '.$this->_iMinTime4Timeline.'s Laufzeit: <strong>'.count($aDataset).'</strong>
+        </p>
+
         <div id="'.$sDivId.'"></div>
 
         <script type="text/javascript">
@@ -124,4 +300,75 @@ class cronlogrenderer extends cronlog{
         return $sHtml;
     }
     
-}
+   /**
+    * show a single log file
+    * 
+    * @param string $sLogfile  logfile; [server]/[filename.log]
+    * @return string
+    */
+   public function renderLogfile($sLogfile){
+        $sHtml='<link rel="stylesheet" type="text/css" href="main.css"><h3>Logfile '.basename($sLogfile).'</h3>'
+                . '<button onclick="showFileBack();">back</button><br><br>'
+                ;
+        if(!$sLogfile){
+            return $sHtml . 'ERROR: empty filename for log file was given.';
+        }
+        if(strstr($sLogfile, '..')){
+            return $sHtml . 'ERROR: wrong log file chars [..] are not allowed.';
+        }
+        $sMyFile=$this->_getServerlogDir().'/'.$sLogfile;
+        if(!file_exists($sMyFile)){
+            return $sHtml . 'ERROR: The requested logfile ['.$sMyFile.'] does not exist (anymore).';
+        }
+
+        
+        if ($fileHandle = fopen($sMyFile, "r")) {
+            $sHtml.='<div style="float: left;" onclick="showFileBack();"><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.'"' : '').'><span class="log-var">'.$sKey.'</span>=<span class="log-value">'.$sValue.'</span></div>';
+                }
+            }
+            $sHtml.='</pre></div>';
+       }
+       return $sHtml;
+   }
+    
+    public function renderServerlist($sSelectedItem=false){
+        $sHtml='';
+        $sHtml.='<option value="ALL">[ALLE]</option>';
+        foreach($this->getServers() as $sServer=>$aData){
+            $sHtml.='<option value="'.$sServer.'"'
+                    .($sSelectedItem===$sServer ? ' selected="selected"' : '')
+                    .'>'.$sServer.'</option>';
+        }
+        $sHtml=$sHtml ? '<select'
+                . ' size="'.(count($this->getServers())+1).'"'
+                // . ' size="1"'
+                . ' onchange="setServer(this.value); return false;"'
+                . '>'.$sHtml.'</select>' : false;
+        return $sHtml;
+    }
+    
+}
\ No newline at end of file
diff --git a/classes/cronlog.class.php b/classes/cronlog.class.php
index fb0e1639a898ba039d5545eb9c6d4dc4fbf7d344..e1fc560867278f12d47a380619bf732e034b17fd 100644
--- a/classes/cronlog.class.php
+++ b/classes/cronlog.class.php
@@ -16,6 +16,7 @@ class cronlog {
     
     
     protected $_sDataDir = "__DIR__/data";
+    protected $_iTtlCache = 60; // in sec
     
     
     protected $_aServers = false;
@@ -58,7 +59,7 @@ class cronlog {
      * @return type
      */
     protected function _getCacheFile($sTaskId){
-        return _getCacheDir().'/'.$sTaskId;
+        return $this->_getCacheDir().'/'.$sTaskId;
     }
 
     /**
@@ -69,6 +70,25 @@ class cronlog {
         return $this->_sDataDir.'/'.$this->_sActiveServer;
     }
 
+    protected function _getCacheData($sTaskId){
+        $sFile=$this->_getCacheFile($sTaskId);
+        if(file_exists($sFile)){
+            if (filemtime($sFile)>(date('U')-$this->_iTtlCache)){
+                // echo "USE cache $sFile<br>";
+                return unserialize(file_get_contents($sFile));
+            } else {
+                // echo "DELETE cache $sFile<br>";
+                unlink($sFile);
+            }
+        }
+        return false;
+    }
+    protected function _writeCacheData($sTaskId, $data){
+        $sFile=$this->_getCacheFile($sTaskId);
+        // echo "WRITE cache $sFile<br>";
+        return file_put_contents($sFile, serialize($data));
+    }
+
     // ----------------------------------------------------------------------
     // public getter
     // ----------------------------------------------------------------------
@@ -135,9 +155,17 @@ class cronlog {
      * get logs from jobfilea of the current or given server
      * @return array
      */
-    public function getServerLogs(){
+    public function getServerJobHistory(){
         $aReturn=array();
+        $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer;
+
+        $aData=$this->_getCacheData($sTaskId);
+        if($aData){
+            return $aData;
+        }
+
         $aData=array();
+            
         foreach(glob($this->_getServerlogDir().'/'.$this->_sFileFilter_serverjoblog) as $sMyJobfile){
             // echo "DEBUG: $sMyJobfile<br>";
             $fileHandle = fopen($sMyJobfile, "r");
@@ -150,7 +178,59 @@ class cronlog {
             }
             fclose($fileHandle);
         }
-        ksort($aReturn);
+        krsort($aReturn);
+        $this->_writeCacheData($sTaskId, $aReturn);
+        return $aReturn;
+    }
+    
+    protected function _parseLogfile($sFile) {
+        $aReturn=array(
+            'SCRIPTNAME'=>false,
+            'SCRIPTTTL'=>false,
+            'SCRIPTSTARTTIME'=>false,
+            'SCRIPTLABEL'=>false,
+            'SCRIPTENDTIME'=>false,
+            'SCRIPTEXECTIME'=>false,
+            'SCRIPTRC'=>false,
+            // 'SCRIPTOUT'=>array(),
+        );
+        $fileHandle = fopen($sFile, "r");
+        while (($line = fgets($fileHandle)) !== false) {
+            // get key ... the part before "="
+            $sKey=trim(preg_replace('/=.*/', '', $line));
+            if($sKey && isset($aReturn[$sKey])){
+                // add value ... the part behind "="
+                $aReturn[$sKey]=preg_replace('/^([A-Z]*\=)/', '', $line);
+            }
+        }
+        fclose($fileHandle);
+        
+        // fetch unit timestamp from date values (they are like "2018-09-30 03:40:05, 1538278805")
+        $aReturn['SCRIPTSTARTTIME']=(int)preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']);
+        $aReturn['SCRIPTENDTIME']=(int)preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']);
+        
+        // remove " s" from exec time value
+        $aReturn['SCRIPTEXECTIME']=preg_replace('/\ s$/', '', $aReturn['SCRIPTEXECTIME']);
+        
+        return $aReturn;
+        
+    }
+    
+    /**
+     * get logs from jobfilea of the current or given server
+     * @return array
+     */
+    public function getServersLastLog(){
+        $aReturn=array();
+        $aData=array();
+        foreach(glob($this->_getServerlogDir().'/'.$this->_sFileFilter_serverlog) as $sMyJobfile){
+            // echo "DEBUG: log file $sMyJobfile<br>";
+            $aData=$this->_parseLogfile($sMyJobfile);
+            $aData['server']=$this->_sActiveServer;
+            $aData['logfile']= $this->_sActiveServer.'/'.basename($sMyJobfile);
+            $aReturn[$aData['SCRIPTSTARTTIME'].$sMyJobfile]=$aData;
+        }
+        rsort($aReturn);
         return $aReturn;
     }
     
@@ -160,8 +240,11 @@ class cronlog {
    
     public function setServer($sServer){
         $this->_sActiveServer=false;
-        if(!array_key_exists($sServer, $this->_aServers)){
-            echo "WARNING: server does not exist<br>";
+        if($sServer==='ALL'){
+            return false;
+        }
+        if($sServer && !array_key_exists($sServer, $this->_aServers)){
+            echo "WARNING: server [$sServer] does not exist<br>";
             return false;
         }
         $this->_sActiveServer=$sServer;
diff --git a/get.php b/get.php
new file mode 100644
index 0000000000000000000000000000000000000000..4fc5e321e3e5589d1def5a58c971b743f4a1d196
--- /dev/null
+++ b/get.php
@@ -0,0 +1,47 @@
+<?php
+/* ======================================================================
+ * 
+ * CRONJOB VIEWER :: AJAX HELPER
+ * 
+ * ======================================================================
+ */
+
+require_once 'classes/cronlog-renderer.class.php';
+
+
+$sMode='html';
+$sItem=isset($_GET['item']) && $_GET['item'] ? $_GET['item'] : false;
+$sServer=isset($_GET['server']) && $_GET['server'] ? $_GET['server'] : false;
+
+
+$sHtml='';
+$oCL = new cronlogrenderer();
+if($sServer){
+    $oCL->setServer($sServer);
+}
+switch ($sItem){
+    case 'crontable':
+        $sHtml.=$oCL->renderJoblist();
+        break;
+    case 'cronlogs':
+        if($sServer==='ALL'){
+            $sHtml.=$oCL->renderCronlogsOfAllServers();
+        } else {
+            $sHtml.=$oCL->renderCronlogs();
+        }
+        break;
+    case 'graph':
+        $sHtml.=$oCL->renderJobGraph();
+        break;
+    case 'selectserver':
+        $sHtml.=$oCL->renderServerlist($sServer);
+        break;
+    case 'showlog':
+        $sLogfile=isset($_GET['logfile']) && $_GET['logfile'] ? $_GET['logfile'] : false;
+        $sHtml.=$oCL->renderLogfile($sLogfile);
+        break;
+    default:
+        header('HTTP/1.0 400 Bad request');
+        die('unknown item ['.$sItem.'] ... or it is not implemented yet.');
+}
+echo $sHtml;
diff --git a/index.php b/index.php
index 551a83ed2ff2482e8fcf55d6d6633735337a726e..cf6b89c611b02a7dbc0fe1d6baa2e3ceced89356 100644
--- a/index.php
+++ b/index.php
@@ -1,39 +1,72 @@
 <?php
-# do spomething here ...
-require_once 'classes/cronlog-renderer.class.php';
 
-$oCL = new cronlogrenderer();
-
-$oCL->getServers();
-$oCL->setServer('kalium');
-$oCL->getServerLogs();
-
-$sHtml='<h2>'.$oCL->getServer().'</h2>'
-        
-        . 'Tabelle der Cronjobs:<br>'
-        . $oCL->renderJoblist($oCL->getServerLogs())
-        
-        . '<br>'
-        . 'Graph (Timeline):<br>'
-        . $oCL->renderJobGraph($oCL->getServerLogs())
-        ;
+// ----- supported query params
+// server=[servername] ... default: ALL
+// q=[filtervalue] ... default: nothing
 
 ?><!doctype html>
 <html><head>
         <title>Cronjob-Viewer</title>
-            <script type="text/javascript" src="vendor/vis/4.21.0/vis.min.js"></script>
-            <link href="vendor/vis/4.21.0/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css"/>
-        <link rel="stylesheet" type="text/css" href="main.css">
-        <!--
-        <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
+        <script type="text/javascript" src="js/functions.js"></script>
+        <script type="text/javascript" src="vendor/jquery/3.2.1/jquery.min.js"></script>
+        
+        <script type="text/javascript" src="vendor/vis/4.21.0/vis.min.js"></script>
+        <link href="vendor/vis/4.21.0/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css"/>
+        
+        <script type="text/javascript" src="vendor/datatables/1.10.15/js/jquery.dataTables.min.js"></script>
+        <link href="vendor/datatables/1.10.15/css/jquery.dataTables.min.css" rel="stylesheet" type="text/css"/>
+        
+        <link href="vendor/font-awesome/5.0.9/web-fonts-with-css/css/fontawesome-all.min.css" rel="stylesheet" type="text/css"/>
         <link rel="stylesheet" type="text/css" href="main.css">
-            <script type="text/javascript" src="vendor/vis/4.21.0/vis.min.js"></script>
-            <link href="vendor/vis/4.21.0/vis-network.min.css" rel="stylesheet" type="text/css"/>
-        -->
     </head>
     <body>
+        <div id="errorlog">
+            
+        </div>
         <h1><span class="imllogo"></span> CronjobViewer</h1>
-    <?php
-        echo $sHtml
-    ?>
+        <nav class="servers">
+            <div id="selectserver" class="active">...</div>
+        </nav>
+        <div id="content">
+
+            <h2 id="lblServername">Server w&auml;hlen...</h2>
+            
+            <nav class="tabs">
+                <a href="#cronlogs"  onclick="setTab(this); return false;" class="active"><i class="far fa-file-alt"></i> Logs</a>
+                <a href="#crontable" onclick="setTab(this); return false;"><i class="fas fa-history"></i> History</a>
+                <a href="#graph"     onclick="setTab(this); return false;"><i class="far fa-chart-bar"></i> Timeline</a>
+            </nav><br><br><br>
+
+            <div id="tabcontent">
+                <div id="cronlogs" class="active"></div>
+                <div id="crontable"></div>
+                <div id="graph"></div>
+            </div>
+
+        </div>
+        <script>
+            
+            // draw navigation with servers
+            getPageItem('selectserver');
+
+            // check query params and set filter
+            // ?server=neon&tab=crontable&q=123
+
+            var oQuery=getQueryParams();
+            var sServer=oQuery.server ? oQuery.server : 'ALL';
+            var q=oQuery.q            ? oQuery.q      : '';
+            var sTab=oQuery.tab       ? oQuery.tab    : '';
+            
+            setServer(sServer);
+
+            if(sTab){
+                var oLink=$('a[href="#'+sTab+'"]').first();
+                console.log(oLink);
+                setTab(oLink);
+            }
+            if (q) {
+                window.setTimeout("$('#datatable1_filter label input').val(q); $('#datatable1').dataTable().fnFilter(q)" , 500);
+                window.setTimeout("$('#datatable2_filter label input').val(q); $('#datatable2').dataTable().fnFilter(q)" , 500);
+            }
+        </script>
 </body></html>
\ No newline at end of file
diff --git a/js/functions.js b/js/functions.js
new file mode 100644
index 0000000000000000000000000000000000000000..83200e1823b73b4fe0f976b2a14a247dc5225114
--- /dev/null
+++ b/js/functions.js
@@ -0,0 +1,101 @@
+
+var sSELECTEDSERVER='';
+var sACTIVESERVERTAB='cronlogs';
+
+
+/**
+ * get query parameters from url as object
+ * @returns {object}
+ */
+function getQueryParams() {
+    qs = document.location.search.split('+').join(' ');
+ 
+    var params = {},
+            tokens,
+            re = /[?&]?([^=]+)=([^&]*)/g;
+ 
+    while (tokens = re.exec(qs)) {
+        params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
+    }
+ 
+    return params;
+}
+
+/**
+ * make an ajax-request and put response content into given div id
+ * @param {string} id  id of the div to be filled
+ * @returns {undefined}
+ */
+function getPageItem(id, sData) {
+	// $('#'+id).html('reading ...');
+        if(!$('#'+id).hasClass('active')){
+            $('#errorlog').html('#'+id+' is not active') ;
+            return false;
+        }
+        var phpscript='get.php';
+        $('#'+id).css('opacity', '0.2');
+        
+        if(!sData){
+            sData='item='+id+'&server='+sSELECTEDSERVER;
+        }
+	jQuery.ajax({
+		url: phpscript,
+		data: sData,
+		type: "GET",
+		success:function(data){
+                    $('#'+id).css('opacity', '1');
+                    $('#'+id).html(data);
+		},
+		error:function(){
+                    $('#'+id).css('opacity', false);
+                    $('#'+id).html('Failed :-/');
+                    $('#errorlog').html(
+                        $('#errorlog').html('AJAX error: <a href="'+phpscript+'?' + sData+'">'+phpscript+'?' + sData+'</a>')
+                    );
+		}
+	});
+}
+
+/**
+ * set server (show its navigation)
+ * tghis action is used after clicking a servername in the navigation
+ * 
+ * @param {string} sServer
+ * @returns {undefined}
+ */
+function setServer(sServer){
+    if(!sServer){
+        return false;
+    }
+    sSELECTEDSERVER=sServer;
+    $('#lblServername').html('<i class="far fa-hdd"></i> '+sSELECTEDSERVER);
+    getPageItem(sACTIVESERVERTAB);
+}
+
+/**
+ * onclick callback for tabs crontable, crontabs, graph
+ * @param {object} oLink  clicked link
+ * @returns {undefined}
+ */
+function setTab(oLink){
+    sTabid=$(oLink).attr('href').replace(/^#/, '');
+    sACTIVESERVERTAB=sTabid;
+
+    $('nav.tabs a').each(function(){
+        $(this).removeClass('active');
+    });
+    $(oLink).addClass('active');
+    $('#tabcontent div').each(function(){
+        $(this).removeClass('active');
+    });
+    $('#'+sTabid).addClass('active');
+
+    setServer(sSELECTEDSERVER);
+}
+
+function showFile(sLogfile){
+    getPageItem(sACTIVESERVERTAB, 'item=showlog&logfile='+sLogfile);
+}
+function showFileBack(){
+    getPageItem(sACTIVESERVERTAB);
+}
\ No newline at end of file
diff --git a/js/functions.min.js b/js/functions.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..12bcd192b4abdb1dc6009a4195b1d5ce8d348b2d
--- /dev/null
+++ b/js/functions.min.js
@@ -0,0 +1,2 @@
+/* https://www.axel-hahn.de/ */
+var sMyServer=false;var oMapHtml2Api={crontable:"crontable",graph:"graph",selectServer:"selectserver"};function getPageItem(c){var b="get.php";$("#"+c).css("opacity","0.2");var a="item="+oMapHtml2Api[c]+"&server="+sMyServer;jQuery.ajax({url:b,data:a,type:"GET",success:function(d){$("#"+c).css("opacity","1");$("#"+c).html(d)},error:function(){$("#"+c).css("opacity",false);$("#"+c).html("Failed :-/");$("#errorlog").html($("#errorlog").html()+'AJAX error: <a href="'+b+"?"+a+'">'+b+"?"+a+"</a><br><br>")}})}function setServer(a){sMyServer=a;$("#lblServername").html(sMyServer);getPageItem("crontable")};
\ No newline at end of file
diff --git a/main.css b/main.css
index 4ef9eb4832ed6eb4e07f747f674d9aa2ea1ef1d4..b0c9e58d679b11226de3aaf781c1bbdba8f43873 100644
--- a/main.css
+++ b/main.css
@@ -22,15 +22,42 @@ footer a{color:#678;}
 i.fa{font-size: 150%; }
 i.fa.lookup{font-size: 100%; opacity: 0.4;}
 
+nav.servers{float: left; margin-right: 1em;}
+
+nav.tabs{}
+nav.tabs a{display: block; float: left; background: rgba(0,60,60,0.05); padding: 0.5em 1em; color:#345; text-decoration: none; margin-right: 2px; border-top: 4px solid rgba(0,0,0,0.01);}
+nav.tabs a.active{background: #cdd; border-color: rgb(255,0,51);; }
+
+option{font-family: verdana,arial; font-size:1.0em; }
+select{border: 0;}
+
 table{border: 1px solid #ccc;}
+tr:hover{background:#eee;}
+th{background:#cdd; padding:0.5em;}
+td{padding:0.3em;}
 
 .imllogo:before {background: rgb(255,0,51);color: #fff;padding: 0.5em 0.3em;content: 'IML'; font-family: "arial"; text-shadow: none;}
 
+/* ----- tabbed content */
+#crontable,#cronlogs,#graph{display: none;}
+#tabcontent div.active{display: block;}
+
+
+/* ----- override datatable defaukts */
+.dataTables_wrapper{clear: none;float: left; margin: auto 1px;}
+table.dataTable tbody tr{background: none;}
+table.dataTable{margin: 0; width: auto;}
+
+
 .message{border: 1px solid rgba(0,0,0,0.1); padding: 1em; float: right;}
-.message-ok{background:#cfc; color:#080;}
-.message-error{background:#fee; color:#800;}
+.message-ok{background:#cfc !important; color:#080 !important;}
+.message-error{background:#fee !important; color:#800 !important;}
 
-/*
+.log-rem{color:#aaa; font-style: italic;}
+.log-var{color:#088;}
+.log-value{color:#008;}
+
+/* timeline
 */
 .vis-item.timeline-result-error{background:#fcc; border-color: #800}
 .vis-item.timeline-result-ok{background:#cfc;border-color: #080}
diff --git a/main.min.css b/main.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..c5bbbd33b46d9397e7aab0f5e15d75154066978e
--- /dev/null
+++ b/main.min.css
@@ -0,0 +1,2 @@
+/* https://www.axel-hahn.de/ */
+a{color:#38a;text-decoration:none}a:hover{text-decoration:underline}body{background:#fff;color:#456;font-family:verdana,arial;font-size:1.0em}button{background:#468;border:0;color:#fff;padding:.5em 1em;border-radius:.3em;border:1px solid rgba(0,0,0,0.1)}button.add{background:#8c8}button.del{background:#c88}button:hover{background:#cde;color:#000}footer{background:rgba(0,60,60,0.05);border-top:1px solid #ccc;padding:1em;margin-top:5em}footer a{color:#678}i.fa{font-size:150%}i.fa.lookup{font-size:100%;opacity:.4}nav{margin-right:1em}option{font-family:verdana,arial;font-size:1.0em}table{border:1px solid #ccc}tr:hover{background:#eee}th{background:#abc;padding:.5em}td{padding:.3em}.imllogo:before{background:#f03;color:#fff;padding:.5em .3em;content:'IML';font-family:"arial";text-shadow:none}.message{border:1px solid rgba(0,0,0,0.1);padding:1em;float:right}.message-ok{background:#cfc;color:#080}.message-error{background:#fee;color:#800}.vis-item.timeline-result-error{background:#fcc;border-color:#800}.vis-item.timeline-result-ok{background:#cfc;border-color:#080}
\ No newline at end of file