diff --git a/config/lang/de.json b/config/lang/de.json
index 1a5ef39f412236844859fc6d0ce224c5575ace9a..2701d0da5376e11c67043aba759f5832904655e2 100644
--- a/config/lang/de.json
+++ b/config/lang/de.json
@@ -214,6 +214,9 @@
     "accept": "Accept",
     "accept-hint": "Accept Phase [%s] und in die Queue von Phase [%s] stellen.",
     "all": "alle",
+    "api-secret": "Secret für API Zugriff",
+    "api-secret-hint": "Hinweise: Bei leeren Secret ist der Zugriff via API deaktiviert. Um den API Zugriff zu aktivieren, ist ein geshartes Secret zu setzen. Ein Neusetzen eines Secrets macht den bisherigen Key ungültig.",
+    "api-secret-generate": "Neues Secret erzeugen",
     "archive": "Archiv",
     "back": "zurück",
     "branch": "Branch/ Tag",
@@ -238,7 +241,7 @@
     "delete": "Löschen",
     "deploy": "Deploy",
     "deploy-configfile": "Konfiguration",
-    "deploy-configfile-hint": "Hier können Variablen in Bash-Syntax hinterlegt werden, die sich vom onbuild oder ondeploy Hook lesen lassen.",
+    "deploy-configfile-hint": "Hier können Variablen in Bash-Syntax hinterlegt werden. Bei einem Build werden diese in [root]/ci-custom-vars geschrieben und lassen sich vom onbuild oder ondeploy Hook lesen.",
     "deploy-hint": "Deploy der Queue von Phase [%s]",
     "deploy-impossible": "Deploy der Queue von Phase [%s] ist nicht möglich.",
     "deploy-settings": "Deployment-Einstellungen",
diff --git a/config/lang/en.json b/config/lang/en.json
index 969d944b89cc275167bb6fadbe8ab47123f1ed0a..ce3575c1c14d687f50108d2aeaa856c8c5b5a693 100644
--- a/config/lang/en.json
+++ b/config/lang/en.json
@@ -216,6 +216,9 @@
     "accept": "Accept",
     "accept-hint": "Accept phase [%s] and put package to the queue of phase [%s].",
     "all": "all",
+    "api-secret": "Secret for API access",
+    "api-secret-hint": "To enable access via API it is required to set a shared secret. If you set a new key then the former key is invalid.",
+    "api-secret-generate": "Generate new secret",
     "archive": "Archive",
     "back": "back",
     "branch": "Branch/ tag",
@@ -240,7 +243,7 @@
     "delete": "Delete",
     "deploy": "Deploy",
     "deploy-configfile": "Configuration",
-    "deploy-configfile-hint": "Here you can place variables in Bash syntax that onbuild oder ondeploy hook can read.",
+    "deploy-configfile-hint": "Here you can place variables in Bash syntax. During the build it will be writen as [root]/ci-custom-vars and is readable in the onbuild oder ondeploy hook.",
     "deploy-hint": "Deploy queue of phase [%s]",
     "deploy-impossible": "Deploy queue of phase [%s] is not possible.",
     "deploy-settings": "Deployment settings",
diff --git a/public_html/api/index.php b/public_html/api/index.php
index aa9d1cc3661cf73f2ad4b5a2bbaa0886ecd21fe7..88ca9c3de1ecd428f21af603e70f8cd8d137cd4b 100644
--- a/public_html/api/index.php
+++ b/public_html/api/index.php
@@ -3,12 +3,12 @@
  * IDEA TO REALIZE:
  * 
  * /api/v1/projects/
- * /api/v1/project/[Name]/[token]/build[/[name-of-branch]]
- * /api/v1/project/[Name]/[token]/accept/[phase]
+ * /api/v1/project/[Name]/build/[name-of-branch]]
+ * /api/v1/project/[Name]/accept/[phase]
  * 
  */
 
-    $bDebug=true;
+    $bDebug=false;
     ini_set('display_errors', 1);
     ini_set('display_startup_errors', 1);
     error_reporting(E_ALL);
@@ -25,23 +25,39 @@
     // ----------------------------------------------------------------------
     // FUNCTIONS
     // ----------------------------------------------------------------------
+    /**
+     * write debug text (if enabled)
+     * @global boolean $bDebug
+     * @param string  $s       message
+     * @param string  $sLevel  level; one of info|
+     * @return boolean
+     */
     function _wd($s, $sLevel='info'){
         global $bDebug;
         if ($bDebug){
-            echo '<div class="debug ">DEBUG: '.$s.'</div>';
+            echo '<div class="debug debug-'.$sLevel.'">DEBUG: '.$s.'</div>';
         }
         return true;
     }
     
+    /**
+     * abort execution with error
+     * @param string   $s        message
+     * @param integer  $iStatus  http status code to send
+     */
     function _quit($s, $iStatus=400){
         $aStatus=array(
            400=>'HTTP/1.0 400 Bad Request', 
-           404=>'HTTP/1.0 403 Access denied', 
+           403=>'HTTP/1.0 403 Access denied', 
+           404=>'HTTP/1.0 404 Not found', 
         );
         header($aStatus[$iStatus]);
         _done(array('status'=>$iStatus, 'info'=>$aStatus[$iStatus], 'message'=>$s));
     }
-    
+    /**
+     * end with OK output
+     * @param type $Data
+     */
     function _done($Data){
         echo is_array($Data) 
             ? json_encode($Data, 1, JSON_PRETTY_PRINT)
@@ -71,14 +87,13 @@
     _wd('<pre>$aUriSplit: '.print_r($aUriSplit, 1).'</pre>');  
     /*
     
-    /api/v1/projects/ci/my-token/build?auth=123
+    /api/v1/projects/ci/build?auth=123
     $aUriSplit: Array
         (
             [0] => v1
             [1] => projects
             [2] => ci
-            [3] => my-token
-            [4] => build
+            [3] => build
         )
      */
     $sApiVersion = isset($aUriSplit[0]) ? $aUriSplit[0] : false;
@@ -107,26 +122,35 @@
                     _wd('<pre>'.print_r($aList,1).'</pre>');
                     _done($aList);
                     break;;
+                    
                 case 'project':
-
+                    // path /api/v1/project
 
                     $sPrjId     = isset($aUriSplit[2]) ? $aUriSplit[2] : false;
-                    $sPrjToken  = isset($aUriSplit[3]) ? $aUriSplit[3] : false;
-                    $sPrjAction = isset($aUriSplit[4]) ? $aUriSplit[4] : false;
+                    $sPrjAction = isset($aUriSplit[3]) ? $aUriSplit[3] : false;
                     _wd('$sPrjId = '.$sPrjId);
-                    _wd('$sPrjToken = '.$sPrjToken);
                     _wd('$sPrjAction = '.$sPrjAction);
                     
-                    $oProject=new project();
-                    if(!in_array($sPrjId, $oProject->getProjects())){
-                        _quit('ERROR: project with id ['.$sPrjId.'] does not exist.');
+                    // try to init the given project
+                    try{
+                        ob_start();
+                        $oProject=new project($sPrjId);
+                        $oProject->setProjectById($sPrjId);
+                        ob_end_clean();
+                            
+                    } catch (Exception $exc) {
+                        _quit('ERROR: project with id ['.$sPrjId.'] does not exist.', 404);
                     }
 
-                    $oProject->setProjectById($sPrjId);
-                    _wd('TODO: verify given token with that in the config.');
-                    
+                    // get secret
                     $aPrjCfg=$oProject->getConfig();
-                    _wd('<pre>'.print_r($aPrjCfg, 1).'</pre>');
+                    $sProjectSecret=isset($aPrjCfg['api']['secret']) ? $aPrjCfg['api']['secret'] : false;
+                    if(!$sProjectSecret){
+                        _quit('Access denied. API access is disabled.');
+                    }
+                    
+                    $aReqHeaders=apache_request_headers();
+                    _wd('<pre>'.print_r($aReqHeaders, 1).'</pre>');
                     
                     break;;
                 default:
diff --git a/public_html/deployment/classes/project.class.php b/public_html/deployment/classes/project.class.php
index 976db48df1314270be79fc3a178d3d976bfa753b..687ba48fb328dedcb0f3e92994f84cfe396cffcf 100644
--- a/public_html/deployment/classes/project.class.php
+++ b/public_html/deployment/classes/project.class.php
@@ -195,8 +195,10 @@ class project extends base {
      * @return boolean
      */
     private function _verifyConfig() {
-        if (!count($this->_aPrjConfig))
-            die(t("class-project-error-no-config"));
+        if (!count($this->_aPrjConfig)){
+            // die(t("class-project-error-no-config"));
+            throw new Exception(t("class-project-error-no-config"));
+        }
 
         if (!array_key_exists("packageDir", $this->_aConfig)) {
             die(t("class-project-error-no-packagedir"));
@@ -3508,7 +3510,25 @@ class project extends base {
                         'cols' => 100,
                         'rows' => 10,
                         'placeholder' => 'export myvariable=&quot;hello world&quot;',
+                    ),
+   
+                    'input' . $i++ => array(
+                        'type' => 'text',
+                        'name' => 'api[secret]',
+                        'label' => t("api-secret"),
+                        'value' => $this->_aPrjConfig["api"]["secret"],
+                        'validate' => 'isastring',
+                        'size' => 100,
+                        'placeholder' => '',
                     ),                    
+                    'input' . $i++ => array(
+                        'type' => 'markup',
+                        'value' => '<div class="col-sm-12">'
+                        . '<p>' . t('api-secret-hint') . '<br>'
+                            . '<a href="#" class="btn btn-default" onclick="$(\'#input'.($i-2).'\').val(generateSecret(64)); return false">'.t("api-secret-generate").'</a>'
+                        . '</p></div>',
+                    ),
+                    
                     // --------------------------------------------------
                     'input' . $i++ => array(
                         'type' => 'markup',
diff --git a/public_html/deployment/js/functions.js b/public_html/deployment/js/functions.js
index c8c2af19bc169bbe1ee0b60ac532ad08b62d7378..8275ea01ccadec30f9249464158a58c0a716b5ce 100644
--- a/public_html/deployment/js/functions.js
+++ b/public_html/deployment/js/functions.js
@@ -1,305 +1,320 @@
-
-/**
- * initialize soft scrolling for links with css class "scroll-link"
- * @see http://css-tricks.com/snippets/jquery/smooth-scrolling/
- * @returns {undefined}
- */
-function initSoftscroll(){
-    $(function() {
-      // $('a[href*=#]:not([href=#])').click(function() {
-      $('a.scroll-link').click(function() {
-        if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
-          var target = $(this.hash);
-          target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
-          if (target.length) {
-            $('html,body').animate({
-              scrollTop: target.offset().top - 70
-            }, 300);
-            return false;
-          }
-        }
-      });
-    });
-}
-
-function showModalMessage(sMessage){
-   $('#divmodalmessage').html(sMessage);
-   $('#divmodal').show();
-}
-function showIdAsModalMessage(sId){
-   var o=$('#'+sId);
-   var sHtml='<a href="#" onclick="return hideModalMessage()" class="btn btn-danger" style="float:right"> X </a>' 
-           + o.html()
-           + '<hr><a href="#" onclick="return hideModalMessage()" class="btn btn-primary"><i class="fa fa-check"></i> OK </a>'
-            ;
-   $('#divmodalmessage').html(sHtml);
-   $('#divmodal').show();
-   return false;
-}   
-
-function hideModalMessage(){
-   $('#divmodal').hide();
-   return false;
-}
-
-// ----------------------------------------------------------------------
-// general init in each page
-// ----------------------------------------------------------------------
-
-$(document).ready(function() {
-    initSoftscroll();
-    // $(".optionName").popover({trigger: "hover"});
-    // $("#content").hide().fadeIn(300);
-});
-
-
-// ----------------------------------------------------------------------
-// action log
-// ----------------------------------------------------------------------
-
-/**
- * get filtered action log table
- * @returns {undefined}
- */
-function updateActionlog(){
-    var sUrlBase="/webservice/?class=Actionlog&action=getLogs&type=json&args=";
-    var aArgs={};
-
-    var aFilteritems=["project", "where", "order", "limit"];
-    var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"];
-    
-    // --- create query url
-    
-    for (i=0; i<aFilteritems.length; i++){
-        sValue=$('#select' + aFilteritems[i]).val();
-        if(sValue){
-            aArgs[aFilteritems[i]]=sValue;
-        }
-    }
-    
-    var sWhere='';
-    for (j=0; j<aTableitems.length; j++){
-        sValue=$('#selectWhere' + aTableitems[j]).val();
-        if(sValue){
-            if (sWhere){
-                sWhere+' AND ';
-            }
-            sWhere+='`'+aTableitems[j]+'`'+sValue;
-        }
-    }
-    if (sWhere) {
-        aArgs["where"]=sWhere;
-    }
-
-    // --- get data
-
-    var sUrl=sUrlBase+'['+JSON.stringify(aArgs)+']';
-    $.post( sUrl, function( aData ) {
-        var sHtml='';
-        
-        // --- generate output
-        if (aData.length && aData[0]["id"]){
-            for (i=0; i<aData.length; i++){
-                sHtml+='<tr class="tractionlogs loglevel-'+aData[i]["loglevel"]+' '+aData[i]["project"]+'">';
-                for (j=0; j<aTableitems.length; j++){
-                    sHtml+='<td>'+aData[i][aTableitems[j]]+'</td>';
-                }
-                sHtml+='</tr>';
-            }
-        }
-        drawTimeline(aData);
-        
-        if (!sHtml){
-            sHtml=sMsgNolog; // variable is set in actionlog.class.php
-        } else {
-            sHead='';
-            for (j=0; j<aTableitems.length; j++){
-                sHead+='<th>'+aTableitems[j]+'</th>';
-            }
-            sHead='<thead><tr>'+sHead+'</tr></thead>';
-            sHtml='<table class="table table-condensed">'+sHead+'<tbody>'+sHtml+'</tbody></table>';
-        }
-        $('#tableLogactions').html(sHtml);
-        filterLogTable();
-    });
-    
-}
-
-/**
- * render timeline with Visjs
- * 
- * @param {array} aData
- * @returns {undefined}
- */
-function drawTimeline(aData){
-    var sDataset='';
-    
-    var container = document.getElementById('divTimeline');
-    if(!container){
-        return false;
-    }
-    container.innerHTML=''; // empty the div
-
-    if (aData.length && aData[0]["id"]){
-        for (i=0; i<aData.length; i++){
-            // keys are 
-            // var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"];
-            sLabel=aData[i]["project"]+'<br>'+aData[i]["action"];
-            sTitle=aData[i]["time"] + '<br>'+aData[i]["loglevel"]+'<br><br>Projekt: ' + aData[i]["project"] +'<br>User: ' + aData[i]["user"] + ' (' + aData[i]["ip"] +')<br>'+ aData[i]["message"] ;
-            sDataset+= (sDataset ? ', ': '' ) 
-                    + '{"id": '+i+', "content": "'+sLabel+'", "start": "'+aData[i]["time"].replace(/\ /, "T") +'", "title": "'+sTitle+'", "group": "'+aData[i]["project"]+'", "className": "loglevel-'+aData[i]["loglevel"]+'"  }';
-        }
-        aDataset=JSON.parse('['+sDataset+']');
-        
-        var items = new vis.DataSet(aDataset);
-        
-        // Configuration for the Timeline
-        var options = {
-             // verticalScroll: false,
-             clickToUse: true
-        };
-
-        // Create a Timeline
-        var timeline = new vis.Timeline(container, items, options);
-    }
-}
-
-/**
-* filter table with action logs by filtertext (input field)
-*/
-function filterLogTable(){
-    var sSearch=$("#efilterlogs").val();
-    var Regex = new RegExp(sSearch, "i");
-    $(".tractionlogs").each(function() {
-        sVisible="none";
-        if ( Regex.exec(this.innerHTML)) {
-            sVisible="";
-        }
-        $(this).css("display", sVisible);
-    });
-    return false;
-}
-
-// ----------------------------------------------------------------------
-// tables
-// ----------------------------------------------------------------------
-var localStoreTablefilter="tblvalue" + location.pathname;
-
-
-// http://blog.mastykarz.nl/jquery-regex-filter/
-jQuery.extend(
-        jQuery.expr[':'], {
-    regex: function (a, i, m) {
-        var r = new RegExp(m[3], 'i');
-        return r.test(jQuery(a).text());
-    }
-}
-);
-
-/*
- highlight v4
- Highlights arbitrary terms.
- 
- <http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html>
- 
- MIT license.
- 
- Johann Burkard
- <http://johannburkard.de>
- <mailto:jb@eaio.com>
- */
-
-jQuery.fn.highlight = function (pat) {
-    function innerHighlight(node, pat) {
-        var skip = 0;
-        if (node.nodeType == 3) {
-            var pos = node.data.toUpperCase().indexOf(pat);
-            if (pos >= 0) {
-                var spannode = document.createElement('span');
-                spannode.className = 'highlight';
-                var middlebit = node.splitText(pos);
-                var endbit = middlebit.splitText(pat.length);
-                var middleclone = middlebit.cloneNode(true);
-                spannode.appendChild(middleclone);
-                middlebit.parentNode.replaceChild(spannode, middlebit);
-                skip = 1;
-            }
-        }
-        else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
-            for (var i = 0; i < node.childNodes.length; ++i) {
-                i += innerHighlight(node.childNodes[i], pat);
-            }
-        }
-        return skip;
-    }
-    return this.length && pat && pat.length ? this.each(function () {
-        innerHighlight(this, pat.toUpperCase());
-    }) : this;
-};
-
-jQuery.fn.removeHighlight = function () {
-    return this.find("span.highlight").each(function () {
-        this.parentNode.firstChild.nodeName;
-        with (this.parentNode) {
-            replaceChild(this.firstChild, this);
-            normalize();
-        }
-    }).end();
-};
-
-
-/**
- * add a filter form to a table
- * @returns {undefined}
- */
-function addFilterToTable(){
-    var sValue=localStorage.getItem(localStoreTablefilter) ? localStorage.getItem(localStoreTablefilter) : '';
-    var sForm='<form class="pure-form">\n\
-        <fieldset>\n\
-            <label for="eFilter">\n\
-                <i class="fa fa-filter"></i> Tabelle filtern\n\
-            </label>\n\
-            <input type="text" id="eFilter" size="40" name="q" placeholder="Suchbegriff..." value="'+sValue+'" onkeypress="filterTable(this);" onkeyup="filterTable(this);" onchange="filterTable(this);">\n\
-            <button class="pure-button" onclick="$(\'#eFilter\').val(\'\'); filterTable(); return false;"><i class="fa fa-times"></i> </button>\n\
-            <span id="filterinfo"></span>\n\
-        </fieldset>\n\
-        </form><div style="clear: both;"></div>';
-    $(sForm).insertBefore($("table").first());
-}
-
-
-/**
- * callback ... filter the table
- * use addFilterToTable() before.
- * @returns {undefined}
- */
-function filterTable() {
-    var filter = $('#eFilter').val();
-    localStorage.setItem(localStoreTablefilter, filter);
-    $("table").removeHighlight();
-    if (filter) {
-        $("tr:regex('" + filter + "')").show();
-        $("tr:not(:regex('" + filter + "'))").hide();
-        $("tr").first().show();
-
-        $("td").highlight(filter);
-    } else {
-        $("td").removeHighlight();
-        $('tr').show();
-    }
-
-    var sInfo = '';
-    var iVisible = -1; // 1 abziehen wg. tr der ersten Zeile
-    $("tr").each(function () {
-        if ($(this).css('display') != 'none') {
-            iVisible++;
-        }
-    });
-
-    sInfo = (iVisible == ($("tr").length - 1))
-            ? "ges.: <strong>" + ($("tr").length - 1) + "</strong> Eintr&auml;ge"
-            : "<strong>" + iVisible + "</strong> von " + ($("tr").length - 1) + " Eintr&auml;gen"
-            ;
-    $('#filterinfo').html(sInfo);
-
-}
+
+/**
+ * initialize soft scrolling for links with css class "scroll-link"
+ * @see http://css-tricks.com/snippets/jquery/smooth-scrolling/
+ * @returns {undefined}
+ */
+function initSoftscroll(){
+    $(function() {
+      // $('a[href*=#]:not([href=#])').click(function() {
+      $('a.scroll-link').click(function() {
+        if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
+          var target = $(this.hash);
+          target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
+          if (target.length) {
+            $('html,body').animate({
+              scrollTop: target.offset().top - 70
+            }, 300);
+            return false;
+          }
+        }
+      });
+    });
+}
+
+function showModalMessage(sMessage){
+   $('#divmodalmessage').html(sMessage);
+   $('#divmodal').show();
+}
+function showIdAsModalMessage(sId){
+   var o=$('#'+sId);
+   var sHtml='<a href="#" onclick="return hideModalMessage()" class="btn btn-danger" style="float:right"> X </a>' 
+           + o.html()
+           + '<hr><a href="#" onclick="return hideModalMessage()" class="btn btn-primary"><i class="fa fa-check"></i> OK </a>'
+            ;
+   $('#divmodalmessage').html(sHtml);
+   $('#divmodal').show();
+   return false;
+}   
+
+function hideModalMessage(){
+   $('#divmodal').hide();
+   return false;
+}
+
+// ----------------------------------------------------------------------
+// general init in each page
+// ----------------------------------------------------------------------
+
+$(document).ready(function() {
+    initSoftscroll();
+    // $(".optionName").popover({trigger: "hover"});
+    // $("#content").hide().fadeIn(300);
+});
+
+
+// ----------------------------------------------------------------------
+// action log
+// ----------------------------------------------------------------------
+
+/**
+ * get filtered action log table
+ * @returns {undefined}
+ */
+function updateActionlog(){
+    var sUrlBase="/webservice/?class=Actionlog&action=getLogs&type=json&args=";
+    var aArgs={};
+
+    var aFilteritems=["project", "where", "order", "limit"];
+    var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"];
+    
+    // --- create query url
+    
+    for (i=0; i<aFilteritems.length; i++){
+        sValue=$('#select' + aFilteritems[i]).val();
+        if(sValue){
+            aArgs[aFilteritems[i]]=sValue;
+        }
+    }
+    
+    var sWhere='';
+    for (j=0; j<aTableitems.length; j++){
+        sValue=$('#selectWhere' + aTableitems[j]).val();
+        if(sValue){
+            if (sWhere){
+                sWhere+' AND ';
+            }
+            sWhere+='`'+aTableitems[j]+'`'+sValue;
+        }
+    }
+    if (sWhere) {
+        aArgs["where"]=sWhere;
+    }
+
+    // --- get data
+
+    var sUrl=sUrlBase+'['+JSON.stringify(aArgs)+']';
+    $.post( sUrl, function( aData ) {
+        var sHtml='';
+        
+        // --- generate output
+        if (aData.length && aData[0]["id"]){
+            for (i=0; i<aData.length; i++){
+                sHtml+='<tr class="tractionlogs loglevel-'+aData[i]["loglevel"]+' '+aData[i]["project"]+'">';
+                for (j=0; j<aTableitems.length; j++){
+                    sHtml+='<td>'+aData[i][aTableitems[j]]+'</td>';
+                }
+                sHtml+='</tr>';
+            }
+        }
+        drawTimeline(aData);
+        
+        if (!sHtml){
+            sHtml=sMsgNolog; // variable is set in actionlog.class.php
+        } else {
+            sHead='';
+            for (j=0; j<aTableitems.length; j++){
+                sHead+='<th>'+aTableitems[j]+'</th>';
+            }
+            sHead='<thead><tr>'+sHead+'</tr></thead>';
+            sHtml='<table class="table table-condensed">'+sHead+'<tbody>'+sHtml+'</tbody></table>';
+        }
+        $('#tableLogactions').html(sHtml);
+        filterLogTable();
+    });
+    
+}
+
+/**
+ * render timeline with Visjs
+ * 
+ * @param {array} aData
+ * @returns {undefined}
+ */
+function drawTimeline(aData){
+    var sDataset='';
+    
+    var container = document.getElementById('divTimeline');
+    if(!container){
+        return false;
+    }
+    container.innerHTML=''; // empty the div
+
+    if (aData.length && aData[0]["id"]){
+        for (i=0; i<aData.length; i++){
+            // keys are 
+            // var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"];
+            sLabel=aData[i]["project"]+'<br>'+aData[i]["action"];
+            sTitle=aData[i]["time"] + '<br>'+aData[i]["loglevel"]+'<br><br>Projekt: ' + aData[i]["project"] +'<br>User: ' + aData[i]["user"] + ' (' + aData[i]["ip"] +')<br>'+ aData[i]["message"] ;
+            sDataset+= (sDataset ? ', ': '' ) 
+                    + '{"id": '+i+', "content": "'+sLabel+'", "start": "'+aData[i]["time"].replace(/\ /, "T") +'", "title": "'+sTitle+'", "group": "'+aData[i]["project"]+'", "className": "loglevel-'+aData[i]["loglevel"]+'"  }';
+        }
+        aDataset=JSON.parse('['+sDataset+']');
+        
+        var items = new vis.DataSet(aDataset);
+        
+        // Configuration for the Timeline
+        var options = {
+             // verticalScroll: false,
+             clickToUse: true
+        };
+
+        // Create a Timeline
+        var timeline = new vis.Timeline(container, items, options);
+    }
+}
+
+/**
+* filter table with action logs by filtertext (input field)
+*/
+function filterLogTable(){
+    var sSearch=$("#efilterlogs").val();
+    var Regex = new RegExp(sSearch, "i");
+    $(".tractionlogs").each(function() {
+        sVisible="none";
+        if ( Regex.exec(this.innerHTML)) {
+            sVisible="";
+        }
+        $(this).css("display", sVisible);
+    });
+    return false;
+}
+
+// ----------------------------------------------------------------------
+// tables
+// ----------------------------------------------------------------------
+var localStoreTablefilter="tblvalue" + location.pathname;
+
+
+// http://blog.mastykarz.nl/jquery-regex-filter/
+jQuery.extend(
+        jQuery.expr[':'], {
+    regex: function (a, i, m) {
+        var r = new RegExp(m[3], 'i');
+        return r.test(jQuery(a).text());
+    }
+}
+);
+
+/*
+ highlight v4
+ Highlights arbitrary terms.
+ 
+ <http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html>
+ 
+ MIT license.
+ 
+ Johann Burkard
+ <http://johannburkard.de>
+ <mailto:jb@eaio.com>
+ */
+
+jQuery.fn.highlight = function (pat) {
+    function innerHighlight(node, pat) {
+        var skip = 0;
+        if (node.nodeType == 3) {
+            var pos = node.data.toUpperCase().indexOf(pat);
+            if (pos >= 0) {
+                var spannode = document.createElement('span');
+                spannode.className = 'highlight';
+                var middlebit = node.splitText(pos);
+                var endbit = middlebit.splitText(pat.length);
+                var middleclone = middlebit.cloneNode(true);
+                spannode.appendChild(middleclone);
+                middlebit.parentNode.replaceChild(spannode, middlebit);
+                skip = 1;
+            }
+        }
+        else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
+            for (var i = 0; i < node.childNodes.length; ++i) {
+                i += innerHighlight(node.childNodes[i], pat);
+            }
+        }
+        return skip;
+    }
+    return this.length && pat && pat.length ? this.each(function () {
+        innerHighlight(this, pat.toUpperCase());
+    }) : this;
+};
+
+jQuery.fn.removeHighlight = function () {
+    return this.find("span.highlight").each(function () {
+        this.parentNode.firstChild.nodeName;
+        with (this.parentNode) {
+            replaceChild(this.firstChild, this);
+            normalize();
+        }
+    }).end();
+};
+
+
+/**
+ * add a filter form to a table
+ * @returns {undefined}
+ */
+function addFilterToTable(){
+    var sValue=localStorage.getItem(localStoreTablefilter) ? localStorage.getItem(localStoreTablefilter) : '';
+    var sForm='<form class="pure-form">\n\
+        <fieldset>\n\
+            <label for="eFilter">\n\
+                <i class="fa fa-filter"></i> Tabelle filtern\n\
+            </label>\n\
+            <input type="text" id="eFilter" size="40" name="q" placeholder="Suchbegriff..." value="'+sValue+'" onkeypress="filterTable(this);" onkeyup="filterTable(this);" onchange="filterTable(this);">\n\
+            <button class="pure-button" onclick="$(\'#eFilter\').val(\'\'); filterTable(); return false;"><i class="fa fa-times"></i> </button>\n\
+            <span id="filterinfo"></span>\n\
+        </fieldset>\n\
+        </form><div style="clear: both;"></div>';
+    $(sForm).insertBefore($("table").first());
+}
+
+
+/**
+ * callback ... filter the table
+ * use addFilterToTable() before.
+ * @returns {undefined}
+ */
+function filterTable() {
+    var filter = $('#eFilter').val();
+    localStorage.setItem(localStoreTablefilter, filter);
+    $("table").removeHighlight();
+    if (filter) {
+        $("tr:regex('" + filter + "')").show();
+        $("tr:not(:regex('" + filter + "'))").hide();
+        $("tr").first().show();
+
+        $("td").highlight(filter);
+    } else {
+        $("td").removeHighlight();
+        $('tr').show();
+    }
+
+    var sInfo = '';
+    var iVisible = -1; // 1 abziehen wg. tr der ersten Zeile
+    $("tr").each(function () {
+        if ($(this).css('display') != 'none') {
+            iVisible++;
+        }
+    });
+
+    sInfo = (iVisible == ($("tr").length - 1))
+            ? "ges.: <strong>" + ($("tr").length - 1) + "</strong> Eintr&auml;ge"
+            : "<strong>" + iVisible + "</strong> von " + ($("tr").length - 1) + " Eintr&auml;gen"
+            ;
+    $('#filterinfo').html(sInfo);
+
+}
+
+
+// ----------------------------------------------------------------------
+// page settings
+// ----------------------------------------------------------------------
+function generateSecret(length){
+
+   var result           = '';
+   var characters       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+   var charactersLength = characters.length;
+   for ( var i = 0; i < length; i++ ) {
+      result += characters.charAt(Math.floor(Math.random() * charactersLength));
+   }
+   return result;
+}
diff --git a/shellscripts/api-client.sh b/shellscripts/api-client.sh
new file mode 100644
index 0000000000000000000000000000000000000000..a4a6f433a16d57cef0c3bf33685239a90346c86e
--- /dev/null
+++ b/shellscripts/api-client.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+# ======================================================================
+#
+# API CLIENT :: proof of concept
+#
+# This is a demo api client
+#
+# ----------------------------------------------------------------------
+# 2020-06-12  first lines  <axel.hahn@iml.unibe.ch>
+# ======================================================================
+
+# ----------------------------------------------------------------------
+# CONFIG
+# ----------------------------------------------------------------------
+
+
+myProject=ci-webgui
+secret="cOiScVAElvcJKmJ1eGrKXZvv6ZROlSgZ9VpSVFK1uxZI8J5ITXuZZb8jIYobuoAB"
+
+myAction=build
+apiHost="http://dev.ci.iml.unibe.ch:8002"
+apiBaseUrl="/api/v1"
+apiMethod=GET
+
+
+# ----------------------------------------------------------------------
+# MAIN
+# ----------------------------------------------------------------------
+
+# --- build url
+apiRequest="${apiBaseUrl}/project/${myProject}/${myAction}"
+
+# --- generate auth
+data="`date`\n${apiMethod}\n${apiRequest}"
+myHash=`echo -n "$data" | openssl sha1 -hmac "${secret}" | cut -f 2 -d" "`
+
+
+
+# https://stackoverflow.com/questions/356705/how-to-send-a-header-using-a-http-request-through-a-curl-call
+
+echo HASH: $myHash ... made from [$data]
+echo REQEST: $apiRequest - $myHash
+
+
+curl -i \
+   -H "Accept: application/json" -H "Content-Type: application/json" \
+   -H "Authorization: demo-bash-client:${myHash}" \
+   -X $apiMethod \
+   ${apiHost}${apiRequest}
+
+rc=$?
+echo rc = $rc
+