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
new file mode 100644
index 0000000000000000000000000000000000000000..4ded2b2a627edcaa53e202ee41d17e94d9b2aebb
--- /dev/null
+++ b/public_html/api/index.php
@@ -0,0 +1,254 @@
+<?php
+/* ======================================================================
+ * 
+ * A P I   F O R   C I   S E R V E R
+ * 
+ * GET  /api/v1/projects/
+ * GET  /api/v1/project/[ID]/build/[name-of-branch]
+ * POST /api/v1/project/[ID]/build/[name-of-branch]
+ * get  /api/v1/project/[ID]/phases
+ * 
+ * ----------------------------------------------------------------------
+ * 2020-06-16  v0.9  <axel.hahn@iml.unibe.ch>  
+ * ======================================================================
+ */
+
+    $bDebug=false;
+    ini_set('display_errors', 1);
+    ini_set('display_startup_errors', 1);
+    error_reporting(E_ALL);
+
+    require_once("../../config/inc_projects_config.php");
+    
+    $sDirClasses=__DIR__.'/../deployment/classes/';
+    require_once($sDirClasses.'/project.class.php');
+    
+    require_once($sDirClasses.'logger.class.php');
+
+    $aApiItems=array(
+        'project',
+        'projects',
+        'help',
+    );
+    $iMaxAge=60;
+    
+    // ----------------------------------------------------------------------
+    // 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-'.$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', 
+           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)
+            : $Data
+            ;
+        die();
+    }
+    
+    /**
+     * Check authorization in the http request header and age of timestamp
+     * On a failed check the request will be terminated
+     * @global int $iMaxAge         max allowed age
+     * @param type $sProjectSecret
+     * @return boolean
+     */
+    function _checkAuth($sProjectSecret){
+        global $iMaxAge;
+        $aReqHeaders=apache_request_headers();
+        _wd('<pre>'.print_r($aReqHeaders, 1).'</pre>');
+        if(!isset($aReqHeaders['Authorization'])){
+            _quit('Access denied. Missing authorization.', 403);
+        }         
+
+        $sGotHash= preg_replace('/^.*\:/', '', $aReqHeaders['Authorization']);
+        $sGotDate= $aReqHeaders['Date'];
+        $sGotMethod=$_SERVER['REQUEST_METHOD'];
+        $sGotReq=$_SERVER['REQUEST_URI'];
+
+
+        $sMyData="${sGotMethod}\n${sGotReq}\n${sGotDate}\n";
+        $sMyHash= base64_encode(hash_hmac("sha1", $sMyData, $sProjectSecret));
+
+        _wd('Hash: '.$sGotHash.' -- from header');
+        _wd('Hash: '.$sMyHash.' -- rebuilt');
+        if($sGotHash!==$sMyHash){
+            _quit('Access denied. Invalid hash.', 403);
+        }
+
+        $iAge=date('U')-date('U', strtotime($sGotDate));
+        _wd('Date: '.$sGotDate.' - age: '.$iAge.' sec');
+        if($iAge>$iMaxAge){
+            _quit('Access denied. Hash is out of date: '.$iAge. ' sec is older '.$iMaxAge.' sec', 403);
+        }
+        return true;
+    }
+    // ----------------------------------------------------------------------
+    // MAIN
+    // ----------------------------------------------------------------------
+
+    _wd('Start: '.date('Y-m-d H:i:s').'<style>body{background:#eee; color:#456;}
+            .debug{background:#ddd; margin-bottom: 2px;}
+         </style>');
+
+    _wd('request uri is '.$_SERVER["REQUEST_URI"]); 
+    _wd('<pre>GET: '.print_r($_GET, 1).'</pre>');
+
+    
+    // ---------- SPLIT URL
+    
+    $aUriSplit= explode('/', preg_replace('/\?.*$/', '', $_SERVER["REQUEST_URI"]));
+
+    array_shift($aUriSplit);
+    array_shift($aUriSplit);     
+    _wd('<pre>$aUriSplit: '.print_r($aUriSplit, 1).'</pre>');  
+    /*
+    
+    /api/v1/projects/ci/build?auth=123
+    $aUriSplit: Array
+        (
+            [0] => v1
+            [1] => projects
+            [2] => ci
+            [3] => build
+        )
+     */
+    $sApiVersion = isset($aUriSplit[0]) ? $aUriSplit[0] : false;
+    $sApiItem    = isset($aUriSplit[1]) ? $aUriSplit[1] : false;
+
+
+    if(!$sApiVersion){
+        _quit('no paramm for api version was found.');
+    }
+    if(!$sApiItem){
+        _quit('ERROR: no paramm for item was found.');
+    }
+    if(!in_array($sApiItem, $aApiItems)){
+       _quit('ERROR: item ['.$sApiItem.'] is invalid.');
+    }
+    
+    
+    
+    switch ($sApiVersion){
+        case 'v1':
+            switch($sApiItem){
+                case 'projects':
+
+                    $oProject=new project();
+                    $aList=$oProject->getProjects();
+                    _wd('<pre>'.print_r($aList,1).'</pre>');
+                    _done($aList);
+                    break;;
+                    
+                case 'project':
+                    // path /api/v1/project
+
+                    $sPrjId     = isset($aUriSplit[2]) ? $aUriSplit[2] : false;
+                    $sPrjAction = isset($aUriSplit[3]) ? $aUriSplit[3] : false;
+                    $sParam4    = isset($aUriSplit[4]) ? $aUriSplit[4] : false;
+                    $sParam5    = isset($aUriSplit[5]) ? $aUriSplit[5] : false;
+                    $sMethod    = $_SERVER['REQUEST_METHOD'];
+                    _wd('$sPrjId = '.$sPrjId);
+                    _wd('$sPrjAction = '.$sPrjAction);
+                    
+                    $oCLog = new logger();
+                    // 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);
+                    }
+
+                    // get secret
+                    $aPrjCfg=$oProject->getConfig();
+                    $sProjectSecret=isset($aPrjCfg['api']['secret']) ? $aPrjCfg['api']['secret'] : false;
+                    if(!$sProjectSecret){
+                        _quit('Access denied. API access is disabled.');
+                    }
+
+                    // check authorization 
+                    _checkAuth($sProjectSecret);
+
+                    // echo "OK: request was authorized successfully.\n";
+
+                    $oProject->oUser->setUser('api');
+
+                    switch($sPrjAction){
+                        case "build":
+                            if ($sParam4){
+                                $aResult=$oProject->setBranchname($sParam4 . ($sParam5 ? '/'.$sParam5 : ''));
+                            }
+                            $sBranchname=$oProject->getBranchname();
+                            $aRepodata = $oProject->getRemoteBranches(true);
+                            if(!isset($aRepodata[$sBranchname])){
+                                _quit('ERROR: branch not found: '.$sBranchname, 404);
+                            }
+                            
+                            
+                            // echo "branch is set to ".$oProject->getBranchname()."\n";
+                            if ($sMethod==='GET'){
+                                $sNext=$oProject->getNextPhase();
+                                _done(array(
+                                    'branch'=>$sBranchname,
+                                    'phase'=>$sNext,
+                                    'repo'=>$aRepodata[$sBranchname]
+                                ));
+                            }
+                            if ($sMethod==='POST'){
+                                echo "starting build() ...";
+                                flush();
+                                echo $oProject->build();
+                            }
+                            break;;
+                        case "phases":
+                            _done($oProject->getAllPhaseInfos());
+                            break;;
+                        default:
+                            _quit('ERROR: Wrong action ['.$sApiItem.'].');
+                    }
+      
+                    break;;
+                    
+                default:
+                    // unreachable - see in_array before switch
+                    _quit('ERROR: item ['.$sApiItem.'] is invalid.');
+            }
+            break;
+        default:
+            _quit('ERROR: Wrong (unsupported) api version.');
+    }
\ No newline at end of file
diff --git a/public_html/deployment/classes/project.class.php b/public_html/deployment/classes/project.class.php
index 976db48df1314270be79fc3a178d3d976bfa753b..dd56aceb4dae443f73a2400b2a40150453bc690e 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"));
@@ -1351,8 +1353,8 @@ class project extends base {
         if ($this->_sProcessTempOut && file_exists($this->_sProcessTempOut)) {
             unlink($this->_sProcessTempOut);
         }
-        $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile);
-        $this->_sProcessTempOut = $sNewTempfile;
+        // $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile);
+        $this->_sProcessTempOut = $sNewTempfile ? sys_get_temp_dir() . "/" . basename($sNewTempfile) : false;
         return $this->_sProcessTempOut;
     }
 
@@ -1450,7 +1452,7 @@ class project extends base {
         }
         return $this->_sBranchname;
     }
-
+        
     // ----------------------------------------------------------------------
     // ACTIONS
     // ----------------------------------------------------------------------
@@ -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/classes/sws.class.php b/public_html/deployment/classes/sws.class.php
index 5da960db10976988d6108636fb9dc48f63538909..433ab7c47eae18fcf53f741415ce1d5b5ce57820 100644
--- a/public_html/deployment/classes/sws.class.php
+++ b/public_html/deployment/classes/sws.class.php
@@ -103,7 +103,7 @@ class sws {
      * version
      * @var string 
      */
-    private $_sVersion = "0.06&nbsp;(beta)";
+    private $_sVersion = "0.07&nbsp;(beta)";
 
     /**
      * title
@@ -166,6 +166,23 @@ class sws {
         return false;
     }
 
+    /**
+     * helper for _parseParams
+     * check if parameter args contain safe chars
+     * @param type $sParamValue
+     * @return boolean
+     */
+    private function _verifyParamValue($sParamValue){
+        $sOKChars='a-z0-9\"\{\}\[\]\.\,\ \:\-\+';
+        if(isset($this->_aParams[$sParamValue])){
+            $sVal=urldecode($this->_aParams[$sParamValue]);
+            if(preg_match('/[^'.$sOKChars. ']/i', $sVal)){
+                $this->_quit("ERROR: parameter $sParamValue=.. contains unsupported character(s): [". preg_replace('/['.$sOKChars. ']/i', '',$sVal)."]");
+            }
+            
+        }
+        return true;
+    }
     /**
      * parse parameters (given GET/ POST is in by _aParams @see setParams)
      *     class  - class to initialize
@@ -176,7 +193,7 @@ class sws {
      */
     private function _parseParams() {
         $aMinParams = array("class", "action");
-        $aMaxParams = array_merge($aMinParams, array("args"));
+        $aMaxParams = array_merge($aMinParams, array("init", "args", "type"));
         $sErrors = '';
 
         // set defaults
@@ -202,7 +219,12 @@ class sws {
                 $sErrors.="- <em>" . $sKey . "</em><br>";
             }
         }
-        // TODO: checkMaxParams
+        // check max Params
+        foreach(array_keys($this->_aParams) as $sKey){
+            if(!in_array($sKey, $aMaxParams)){
+                $this->_quit("ERROR: parameter ". htmlentities($sKey)." is unknown.");
+            }
+        }
         
         // check if classname and action exist in configuration
         if (array_key_exists("class", $this->_aParams)) {
@@ -214,6 +236,9 @@ class sws {
                 $this->_sClassfile = $this->_aKnownClasses["classes"][$this->_aParams["class"]]["file"];
 
                 // get arguments for the method
+                $this->_verifyParamValue("init");
+                $this->_verifyParamValue("args");
+                
                 if (array_key_exists("init", $this->_aParams)) {
                     try {
                         $aTmp = json_decode($this->_aParams["init"], 1);
@@ -222,7 +247,7 @@ class sws {
                     }
                     if (!is_array($aTmp)) {
                         $this->_quit(
-                                'ERROR: wrong request - init value must be a json string<br>'
+                                'ERROR: wrong request - init value must be a json string (in url encoded form)<br>'
                                 . 'examples:<br>'
                                 . '- one arg <code>(...)&init=["my string"]</code><br>'
                                 . '- two args <code>(...)&init=["my string", 123]</code> '
@@ -249,7 +274,7 @@ class sws {
                 }
                 if (!is_array($aTmp)) {
                     $this->_quit(
-                            'ERROR: wrong request - args value must be a json string<br>'
+                            'ERROR: wrong request - args value must be a json string (in url encoded form)<br>'
                             . 'examples:<br>'
                             . '- one arg <code>(...)&args=["my string"]</code><br>'
                             . '- two args <code>(...)&args=["my string", 123]</code> '
@@ -319,7 +344,7 @@ class sws {
      * @param type $sError
      */
     private function _quit($sError, $sMore = "") {
-        header("Status: 400 Bad Request");
+        header("HTTP1.0 400 Bad Request");
         echo $this->_wrapAsWebpage(
                 $sMore, '<div class="error">' . $sError . '</div>'
         );
@@ -623,7 +648,7 @@ class sws {
         $sClassInit = '<span class="urlvalue">[initparams]</span>';
         $sActionSelect = '<span class="urlvalue">[action]</span>';
         $sParamSelect = '<span class="urlvalue">[parameters]</span>';
-        $sTypeSelect = '<span class="urlvalue">[type: raw|json]</span>';
+        $sTypeSelect = '<span class="urlvalue">[raw|json]</span>';
 
         $sSyntax = sprintf(
                 '<pre>?'
@@ -701,6 +726,9 @@ class sws {
                     . '.warning{ color:#a96; background:#fc8; padding: 0.5em; margin-bottom: 2em; border-left: 4px solid;}'
                     . '.defaultvalue{color: #33c;}'
                 . '</style>'
+                ;
+        if($this->_aOptions["enableGui"]){
+            $sReturn.= ''
                 . '<script>'
                 . 'function toggleDesciption(sId, a){'
                 . 'var o=document.getElementById(sId);'
@@ -781,6 +809,9 @@ class sws {
                     
                   '
                 . '</script>'
+                ;
+        }
+            $sReturn.= ''
                 . '</head>'
                 . '<body>';
 
diff --git a/public_html/deployment/classes/user.class.php b/public_html/deployment/classes/user.class.php
index da03b92599e7a37c54f77066a3c5bf94f1b0febf..756b6ae9efa712f02f0fb60c13297a8d23fffe00 100644
--- a/public_html/deployment/classes/user.class.php
+++ b/public_html/deployment/classes/user.class.php
@@ -53,15 +53,15 @@ class user {
     
     
     /**
-     * detect a user
-     * @return type
+     * get string with detected user from current session / basic auth / cli access
+     * @return string
      */
     private function _autoDetectUser(){
         $sUser=false;
-        if (isset($_SESSION) && is_array($_SESSION) && array_key_exists("PHP_AUTH_USER", $_SESSION)){
+        if (isset($_SESSION) && isset($_SESSION["PHP_AUTH_USER"])){
             $sUser=$_SESSION["PHP_AUTH_USER"];
         }
-        if (!$sUser && is_array($_SERVER) && array_key_exists("PHP_AUTH_USER", $_SERVER)){
+        if (!$sUser && isset($_SERVER["PHP_AUTH_USER"])){
             $sUser=$_SERVER["PHP_AUTH_USER"];
         }
         if (php_sapi_name() == "cli") {
@@ -137,7 +137,7 @@ class user {
      */
     public function authenticate(){
         global $aConfig, $aParams;
-        
+        print_r($aConfig);
         if(!array_key_exists('auth', $aConfig) || !count($aConfig['auth']) || !array_key_exists('user', $aParams)){
             return false;
         }
@@ -162,7 +162,7 @@ class user {
                 
                 // set a session - it must correspondent with _autoDetectUser()
                 $_SESSION["PHP_AUTH_USER"]=$sUser;
-                $this->setUser();
+                $this->setUser('');
                 return true;
             }
         }
@@ -180,10 +180,15 @@ class user {
     }
     
     /**
-     * set a authenticated user and get its roles
+     * set an authenticated user and get its roles
      */
-    public function setUser(){
-        $this->_sUsername=$this->_autoDetectUser();
+    public function setUser($sUser=false){
+        if($sUser!==false){
+            $this->_sUsername=$sUser;
+            $_SESSION["PHP_AUTH_USER"]=$sUser;
+        } else {
+            $this->_sUsername=$this->_autoDetectUser();
+        }
         $this->_getUserGroups();
         $this->_getUserPermission();
     }
diff --git a/public_html/deployment/classes/vcs.git.class.php b/public_html/deployment/classes/vcs.git.class.php
index c994e34251c07740041a30b773ed24b142237d53..d361b364e4071507b2cec9b65ac115b92a399c76 100644
--- a/public_html/deployment/classes/vcs.git.class.php
+++ b/public_html/deployment/classes/vcs.git.class.php
@@ -357,6 +357,9 @@ class vcs implements iVcs {
         } else {
             $a = $this->getRevision(false);
         }
+        if(!isset($a['branch'])){
+            return false;
+        }
         // merge with cached info ... to add type and label
         if(isset($this->_aRemoteBranches[$a['branch']])){
             $this->_aRemoteBranches[$a['branch']]=array_merge($this->_aRemoteBranches[$a['branch']], $a);
diff --git a/public_html/deployment/js/functions.js b/public_html/deployment/js/functions.js
index c8c2af19bc169bbe1ee0b60ac532ad08b62d7378..87a3db1e183204992d7ea58adf1edcfd5bca24fc 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);
+
+}
+
+
+// ----------------------------------------------------------------------
+// API secret
+// ----------------------------------------------------------------------
+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/public_html/webservice/sws-config.json b/public_html/webservice/sws-config.json
index a7b7e3273c59e06c4baf89e8dfb4962564307f84..a2cdf80e167968735114c65e56b12c18bc165851 100644
--- a/public_html/webservice/sws-config.json
+++ b/public_html/webservice/sws-config.json
@@ -1,7 +1,7 @@
 {
     "options": {
         "enableGui": 1,
-        "enableDump": 1
+        "enableDump": 0
     },
     "classes": {
         "Actionlog": {
@@ -9,6 +9,12 @@
             "actions": {
                 "getLogs": {}
             }
+        },
+        "project": {
+            "file": "project.class.php",
+            "actions": {
+                "getProjects": {}
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/shellscripts/api-client.sh b/shellscripts/api-client.sh
new file mode 100644
index 0000000000000000000000000000000000000000..25a881567b012a52f398fffdd99adeb70463dd4e
--- /dev/null
+++ b/shellscripts/api-client.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+# ======================================================================
+#
+# API CLIENT :: proof of concept
+#
+# This is a demo api client
+#
+# ----------------------------------------------------------------------
+#
+# my projects and secrets
+# DATA:ci-webgui:cOiScVAElvcJKmJ1eGrKXZvv6ZROlSgZ9VpSVFK1uxZI8J5ITXuZZb8jIYobuoAB
+#
+# ----------------------------------------------------------------------
+# 2020-06-12  first lines  <axel.hahn@iml.unibe.ch>
+# ======================================================================
+
+# ----------------------------------------------------------------------
+# CONFIG
+# ----------------------------------------------------------------------
+
+apiHost="http://dev.ci.iml.unibe.ch:8002"
+line="----------------------------------------------------------------------"
+
+
+# ----------------------------------------------------------------------
+# FUNCTIONS
+# ----------------------------------------------------------------------
+
+function showhelp(){
+echo "
+SYNTAX
+   projects     show projects
+
+   buildinfo [project] [branch]   
+                show infos about what happens on build
+
+   build [project] [branch]
+                execute build
+
+   phases [project]
+                show status of phases
+
+PARAMETERS:
+  project  project id in ci; see output or projects
+  branch   name of branch (optional), i.e. origin/feature-123
+"
+}
+
+
+function makeRequest(){
+
+    local apiMethod=$1
+    local apiRequest=$2
+    local secret=$3
+
+    echo $apiMethod ${apiHost}${apiRequest}
+    echo $line
+
+    if [ ! -z $secret ]; then
+
+        # --- date in http format
+        LANG=en_EN
+        # export TZ=GMT
+        apiTS=`date "+%a, %d %b %Y %H:%M:%S %Z"`
+
+
+# --- generate data to hash: method + uri + timestamp; delimited with line break
+data="${apiMethod}
+${apiRequest}
+${apiTS}
+"
+
+        # generate hash - split in 2 commands (piping "cut" sends additional line break)
+        myHash=`echo -n "$data" | openssl sha1 -hmac "${secret}" | cut -f 2 -d" "`
+        myHash=`echo -n "$myHash" | base64`
+
+        curl -i \
+           -H "Accept: application/json" -H "Content-Type: application/json" \
+           -H "Date: ${apiTS}" \
+           -H "Authorization: demo-bash-client:${myHash}" \
+           -X $apiMethod \
+           ${apiHost}${apiRequest} 
+    else
+        curl -i \
+           -H "Accept: application/json" -H "Content-Type: application/json" \
+           -X $apiMethod \
+           ${apiHost}${apiRequest} 
+    fi
+}
+
+
+# ----------------------------------------------------------------------
+# MAIN
+# ----------------------------------------------------------------------
+
+if  [ $# -lt 1 ]; then
+    showhelp
+    exit 1
+fi
+
+myProject=$2
+secret=`grep "^#\ DATA:${myProject}:" $0 | cut -f 3- -d ":"`
+
+
+case $1 in
+
+    # --- projects is an access without autorization
+    "projects")
+        makeRequest GET  /api/v1/projects
+        ;;
+
+    # --- access WITH autorization only
+    "build")
+        makeRequest POST /api/v1/project/$myProject/build/$3 "$secret"
+        ;;
+    "buildinfo")
+        makeRequest GET  /api/v1/project/$myProject/build/$3 "$secret"
+        ;;
+    "phases")
+        makeRequest GET  /api/v1/project/$myProject/phases   "$secret"
+        ;;
+    *)
+        echo "ERROR: unknown parameter $1"
+        exit 2
+esac
+
+rc=$?
+echo
+echo $line
+echo rc=$rc
+
+exit $rc
\ No newline at end of file