diff --git a/docs/images/screenshot_main.png b/docs/images/screenshot_main.png
index 7825f38a784c6b8f57ddaea7e16d5007d849ba7b..897e4b1215fcf16e412e7be6506f450d9c126e5b 100644
Binary files a/docs/images/screenshot_main.png and b/docs/images/screenshot_main.png differ
diff --git a/public_html/index.html b/public_html/index.html
index b1f5963c84e31944a24e715e8bcde51d034f5f9c..49055b1d9ec4183a997bd5845651477c95626180 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -13,10 +13,12 @@
 
         <header>
             <h1><span>📋</span> Appmonitor Dashboard</h1>
+            <p id="header-section"></p>
         </header>
-
-        <section id="tag-section"></section>
-        <section id="app-section"></setion>
+        
+        <h2><span>📢</span> App status</h2>
+        <section id="tag-section"><h2><span>⏳</span> Please wait ...</h2></section>
+        <section id="app-section"></section>
 
     </div>
 
@@ -26,6 +28,7 @@
         <a href="https://github.com/iml-it/appmonitor" target="_blank">Appmonitor</a>
     </footer>
 
+    <script src="javascript/ubd.class.js" type="text/javascript"></script>
     <script src="javascript/inc_config.js" type="text/javascript"></script>
     <script src="javascript/functions.js" type="text/javascript"></script>
 
diff --git a/public_html/javascript/functions.js b/public_html/javascript/functions.js
index ea86bb88ad9c1665801cc3faf8206bca109e9b82..69497a179787593fdd88ea0bcbe6df4cb32bdf21 100644
--- a/public_html/javascript/functions.js
+++ b/public_html/javascript/functions.js
@@ -1,6 +1,6 @@
 /*
 
-
+    IML APPMONITOR DASHBOARD
 
 */
 
@@ -16,32 +16,34 @@ const AM_RESULTS={
 };
 
 const AM_ICONS={
-    'wait': '⏳',
+    'connect': '🔌',
     'tag': '🏷️',
-    'app': '🪧',
     'delete': '❌',
-    'result0': '✔',
-    'result1': '❔',
-    'result2': '❗',
-    'result3': '🔴',
-
 };
+const OUT_ID_MAIN='header-section';
 const OUT_ID_APPS='app-section';
 const OUT_ID_TAGS='tag-section';
 const ID_TAGINPUT='E_TAGS';
 
+// callback object after changing a tag filter
+const FILTER_CALLBACK="oUbdApps.update()";
+
 // ----------------------------------------------------------------------
 // VARS
 // ----------------------------------------------------------------------
 
 var AM_TAGURL=false;
 var AM_PRETTYURL=false;
-var AM_TIMER=false;
 
 // ----------------------------------------------------------------------
 // FUNCTIONS
 // ----------------------------------------------------------------------
 
+/**
+ * generate a api url to the appmonitor
+ * @param {string} sPath 
+ * @returns 
+ */
 function _getAMApiUrl(sPath){
     return AM_PRETTYURL 
         ? AM_SERVER_URL+sPath
@@ -62,7 +64,15 @@ function _getUrlForTags(){
  * @param {string} tags list of tags - separated by comma
  * @returns 
  */
-function _getUrlWithTags(tags){
+function _getUrlWithTags(){
+    var o=document.getElementById(ID_TAGINPUT);
+    var tags="";
+    if(o) { 
+        tags+=o.value
+    } else { 
+        tags+=AM_TAGS 
+    };
+
     return  _getAMApiUrl('/v1/apps/tags/'+tags+'/all');
     }
 
@@ -89,10 +99,15 @@ function _2digits(i){
 // FUNCTIONS :: TAGS
 // ----------------------------------------------------------------------
 
-
+/**
+ * toggle a tag
+ * callback function of a button with a tagname
+ * @param {string} tagname 
+ */
 function tagToggle(tagname){
     var o=document.getElementById(ID_TAGINPUT);
-    var s=o.value;
+    var sLast=o.value;
+    var s=sLast;
     var re = new RegExp(",*"+tagname);
     var s2=s.replace(re, "");
     if(s2!==s) {
@@ -101,78 +116,53 @@ function tagToggle(tagname){
     } else {
         s+=(s ? "," : "" ) + tagname;
     }
-    o.value=s;
-    getAppstatus();
+
+    if(s!==sLast){
+        o.value=s;
+        // execute update of the app list
+        eval(FILTER_CALLBACK);
+    }
 }
 
+/**
+ * clear all tags
+ * callback function of the [x] button
+ */
 function tagClear(){
     var o=document.getElementById(ID_TAGINPUT);
     o.value="";
-    getAppstatus();
+
+    // execute update of the app list
+    eval(FILTER_CALLBACK);
 }
 
 /**
  * called from getTags
- * @param {string} sData JSOM Response
+ * @param {object} aData JSON Response
  * @returns 
  */
-function _getTaglist(sData){
+function _getTaglist(aData){
     var sReturn='';
     var sTags='';
-    let aData=JSON.parse(sData);
+    if(!aData['tags']){
+        aData=JSON.parse(aData);
+    }
 
     // sReturn+='<code>'+sData+'</code><br>';
     sReturn+='<input id="'+ID_TAGINPUT+'" type="text" size="20" value="'+AM_TAGS+'"'
-            +' onkeypress="getAppstatus()"'
-            +' onkeydown="getAppstatus()"'
-            +' onkeyup="getAppstatus()"'
-        +'>'
+            +' onkeyup="eval(FILTER_CALLBACK)"'
+            +' onchange="eval(FILTER_CALLBACK)"'
+            +'>'
         +'<button onclick="tagClear();return false;"> ❌ </button><br>'
         ;
 
     for (var s in aData['tags']){
         sTags+=(sTags ? ",": "") + aData['tags'][s];
-        sReturn+='<button onclick="tagToggle(\''+aData['tags'][s]+'\'); return false;">'+aData['tags'][s]+'</button>'
+        sReturn+='<button onclick="tagToggle(\''+aData['tags'][s]+'\'); return false;">'+AM_ICONS['tag'] + ' ' + aData['tags'][s]+'</button>'
     }
     return sReturn;
 }
 
-
-
-/**
- * fetch appmonitor api - taglist
- * called from getAppstatus
- */
- async function getTags(){
-    AM_TAGURL=_getUrlForTags();
-
-    let out = '<h2><span>'+AM_ICONS['tag']+'</span> Tags</h2>';
-
-    try{
-        let response = await fetch(AM_TAGURL, { "headers": 
-            AM_AUTH 
-        } );
-
-        // let response = await fetch(AM_TAGURL);
-        if (response.ok) {
-            out+=_getTaglist(await response.text());
-        } else {
-            out+='<div class="app result1">'
-                +'ERROR '+response.status+': '+response.statusText + ' - '
-                +AM_TAGURL
-                +'</div>';
-        } 
-    } catch {
-        out+='<div class="app result1">'
-        +'UNKNOWN: no response from '
-        +AM_TAGURL
-        +'</div>';
-    }
-    
-    document.getElementById(OUT_ID_TAGS).innerHTML=out;
-}
-
-
 // ----------------------------------------------------------------------
 // FUNCTIONS :: APPS
 // ----------------------------------------------------------------------
@@ -182,19 +172,19 @@ function _getTaglist(sData){
  * @param {string}  sData  response body from api request (JSON string)
  * @returns string
  */
-function _getAllAppsStatus(sData){
+function _getAllAppsStatus(aAllData){
     var sReturn="";
     var oDate=new Date;
     // sReturn+='<code>'+sData+'</code><br>';
     sReturn+=''
-            +'<h3>Tags: ' + tags + '</h3>'
-            + '<p>' 
+            // +'<h3>Tags: ' + tags + '</h3>'
+            // + '<p>' 
             +_2digits(oDate.getHours())
             +':'+_2digits(oDate.getMinutes())
             +':'+_2digits(oDate.getSeconds())
             +' (update every '+REFRESHTIME+' sec)'
-        +'</p>';
-    let aAllData=JSON.parse(sData);
+        // +'</p>';
+    // let aAllData=JSON.parse(sData);
     for (var key in aAllData){
         sReturn+=_getSingleAppStatus(aAllData[key]);
     }
@@ -216,30 +206,35 @@ function toggleAppDetails(oLink){
  * @returns string
  */
 function _getSingleAppStatus(aData){
-    // let aData=JSON.parse(sData);
 
+    if(!aData.result){
+        return '';
+    }
     // DEBUG
+    // console.log("----- _getSingleAppStatus")
     // console.log(aData);
 
     // ------ checks
     let sChecks='';
-    for (var j=0; j<aData.checks.length; j++){
-        let tmpCheck=aData.checks[j];
-        sChecks+='<tr class="result'+tmpCheck.result+'">'
-            +'<td>'+tmpCheck.name+'</td>'
-            +'<td>'+tmpCheck.description+'</td>'
-            +'<td>'+tmpCheck.value+'</td>'
-            +'</tr>'
-            ;
+    if(aData.checks) {
+        for (var j=0; j<aData.checks.length; j++){
+            let tmpCheck=aData.checks[j];
+            sChecks+='<tr class="result'+tmpCheck.result+'">'
+                +'<td>'+tmpCheck.name+'</td>'
+                +'<td>'+tmpCheck.description+'</td>'
+                +'<td>'+tmpCheck.value+'</td>'
+                +'</tr>'
+                ;
+        }
     }
-    sChecks=sChecks ? '<table>'+sChecks+'</table>' : ' (No checks were found)';
+    sChecks=sChecks ? '<table class="checks">'+sChecks+'</table>' : ' (No checks were found)';
 
     // ----- generate output
-    let sReturn='<div class="app result'+ aData.meta.result +'">'
+    let sReturn='<div class="app result'+ aData.result.result +'">'
         +'<div class="title" onclick="toggleAppDetails(this);">'
-            +'<span class="float-right">'+AM_RESULTS[aData.meta.result]+'</span>'
+            +'<span class="float-right">'+AM_RESULTS[aData.result.result]+'</span>'
             +'<span class="float-right url">'+aData.result.url+'</span>'
-            +aData.meta.website
+            +aData.result.website
         +'</div>'
         ;
 
@@ -247,15 +242,16 @@ function _getSingleAppStatus(aData){
     let iAge=Math.round((new Date()).getTime() / 1000)- aData.result.ts;
 
     sReturn+=''
-        +'<table class="details" style="display: none;">'
-        +_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.meta.result] 
-            + ' | Checks: ' + aData.checks.length 
+        +'<div class="details" style="display: none;">'
+        +'<br><table>'
+        +_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.result.result] 
+            // + ' | Checks: ' + aData.checks.length 
             + ' | Age: ' + iAge + ' sec'
             + ' | TTL: ' + aData.result.ttl 
             )
         +_appItem('Checks', sChecks)
         
-        +'</table>'
+        +'</table></div>'
         ;
 
     sReturn+='</div>';
@@ -263,62 +259,48 @@ function _getSingleAppStatus(aData){
     return sReturn;
 }
 
-/**
- * fetch appmonitor api - status of apps
- */
-async function getAppstatus(){
-
-
-    var o=document.getElementById(ID_TAGINPUT);
-    tags=o ? o.value : AM_TAGS;
-
-    let out = '<h2><span>'+AM_ICONS['app']+'</span> Applications</h2>';
-    let apiurl=_getUrlWithTags(tags)
-    // let myfunction="_getAllAppsStatus";
-    try{
-        let response = await fetch(apiurl, { "headers": 
-            AM_AUTH 
-        } );
-        if (response.ok) {
-
-            out+=_getAllAppsStatus(await response.text());
-            // out+=eval(myfunction+'(await '+response.text()+')');
-            
-        } else {
-            out+='<div class="app result1">'
-                +'ERROR '+response.status+': '+response.statusText + ' - '
-                +apiurl
-                +'</div>';
-        } 
-    }
-    catch {
-        out+='<div class="app result1">'
-        +'UNKNOWN: no response from '
-        +apiurl
-        +'</div>';
-    }
-    // }
-    document.getElementById(OUT_ID_APPS).innerHTML=out;
-    clearTimeout(AM_TIMER);
-    AM_TIMER=window.setTimeout("getAppstatus()", REFRESHTIME*1000);
-}
-
-
 // ----------------------------------------------------------------------
 // MAIN
 // ----------------------------------------------------------------------
 
 // for auth header with basic auth
-let AM_AUTH=(AM_USER)
+let oHeaders=(AM_USER)
     ? { "Authorization": "Basic " + btoa(AM_USER + ":" + AM_PASSWORD) }
     : {}
 ;
 
-document.getElementById(OUT_ID_TAGS).innerHTML='<h2><span>'+AM_ICONS['wait']+'</span> wait ...</h2>';
-document.getElementById(OUT_ID_APPS).innerHTML='<h2><span>'+AM_ICONS['wait']+'</span> wait ...</h2>';
+document.getElementById(OUT_ID_MAIN).innerHTML=''+AM_ICONS['connect']+' Connected instance <a href="'+AM_SERVER_URL+'">'+AM_SERVER_URL+'</a>';
+
+// initialize tags
+var oUbdTag=new ubd(
+    {
+        'domid':     OUT_ID_TAGS,
+        'url':       _getUrlForTags(),
+        'header':    { "headers": oHeaders },
+        'renderer':  _getTaglist,
+        'ttl':       0,
+    }
+);
+
+// initialize visible apps
+var oUbdApps=new ubd(
+    {
+        'domid':     OUT_ID_APPS,
+        'url':       _getUrlWithTags, // remark: this is a function
+        'header':    { "headers": oHeaders },
+        'renderer':  _getAllAppsStatus,
+        'ttl':       0,
+    }
+);
+
+
+
+// fill in initial values
+oUbdTag.update();
+oUbdApps.update();
 
-getTags();
-AM_TIMER=window.setTimeout("getAppstatus()", 500);
+// cyclic updates of the app status view
+window.setInterval("oUbdApps.update();", REFRESHTIME*1000);
 
 
 // ----------------------------------------------------------------------
\ No newline at end of file
diff --git a/public_html/javascript/inc_config.js.dist b/public_html/javascript/inc_config.js.dist
index 4fe918905914b39d5889ac98c0dda57e6963e1d5..fb490522c81e494072c063dc6741e2e062ed113b 100644
--- a/public_html/javascript/inc_config.js.dist
+++ b/public_html/javascript/inc_config.js.dist
@@ -2,7 +2,7 @@ const AM_SERVER_URL='https://appmonitor.example.com/api';
 const AM_TAGS='live,myapp';
 
 // optional: BASIC AUTH
-// const AM_USER='api';
-// const AM_PASSWORD='password-of-api-user';
+const AM_USER='';
+const AM_PASSWORD='';
 
 const REFRESHTIME=30; // in sec
diff --git a/public_html/javascript/ubd.class.js b/public_html/javascript/ubd.class.js
new file mode 100644
index 0000000000000000000000000000000000000000..49f54a4c86762f48bc34456ef0d4b7a3fc68bab5
--- /dev/null
+++ b/public_html/javascript/ubd.class.js
@@ -0,0 +1,197 @@
+/**
+ * ======================================================================
+ * 
+ *        U B D
+ * 
+ *        Url binded to a dom id
+ * 
+ * ----------------------------------------------------------------------
+ * 
+ * This is a helper class to update the content of a dom object by a 
+ * given Url after a ttl.
+ * 
+ * ----------------------------------------------------------------------
+ * 2022-06-26  www.axel-hahn.de  first lines...
+ * ======================================================================
+ */
+
+var ubd = function(){
+
+    this._sDomId='';
+    this._oDomObject=false;
+    this._sUrl2Fetch=false; // static value or reference of a function
+    this._oHeader={};
+    this._sRenderfunction=false;
+    this._iTTL=false;
+
+    this._oTimer=false;
+
+    this._body='';
+        
+    /**
+     * initialize data for a dom object
+     * @parm  {object}  oConfig    optional config object with those subkeys:
+     *                                  domid    - id of a dom object
+     *                                  url      - url to an api
+     *                                  header   - http request header data
+     *                                  renderer - renderer function to visualize data
+     *                                  ttl      - ttl in sec (TODO)
+     * @returns {undefined}
+     */
+    this.init = function(oConfig){
+        if(oConfig['domid']){
+            this.setDomid(oConfig['domid']);
+        }
+        if(oConfig['url']){
+            this.setUrl(oConfig['url']);
+        }
+        if(oConfig['header']){
+            this.setHeaders(oConfig['header']);
+        }
+        if(oConfig['renderer']){
+            this.setRenderfunction(oConfig['renderer']);
+        }
+        if(oConfig['ttl']){
+            this.setTtl(oConfig['ttl']);
+        }
+    },
+
+    // ----------------------------------------------------------------------
+    // public SETTER for properties
+    // ----------------------------------------------------------------------
+
+    /**
+     * set domid that will by updated
+     * @param {string} sDomid if of a domobject
+     */
+    this.setDomid = function(sDomid){
+        if (document.getElementById(sDomid)){
+            this._sDomId=sDomid;
+            this._oDomObject=document.getElementById(sDomid);
+        } else {
+            this._sDomId=false;
+            this._oDomObject=false;
+            console.error('ERROR: setDomid("'+sDomid+'") got an invalid string - this domid does not exist.');
+        }
+    },
+
+
+    /**
+     * set a rendering function that visualized data after a http request
+     * @param {string|function} oFunction 
+     */
+    this.setRenderfunction = function(oFunction){
+        this._sRenderfunction=oFunction;
+    },
+
+    /**
+     * Set time to live in seconds
+     * @param {int} iTTL 
+     */
+    this.setTtl = function(iTTL){
+        this._iTTL=iTTL/1;
+        this.resetTimer();
+    },
+
+    /**
+     * set an url to be requested
+     * @param {string|function} sUrl  static value or reference of a function
+     */
+    this.setUrl = function(sUrl){
+        this._sUrl2Fetch=sUrl;
+    },
+
+    /**
+     * set header obejct for 2nd param in javascript  fetch() function
+     * @param {object} oHeader 
+     */
+    this.setHeaders = function(oHeader){
+        this._oHeader=oHeader;
+    },
+
+    /**
+     * helper: dump current object instance to console
+     */
+    this.dumpme = function(){
+        console.log('---------- DUMP ubd');
+        console.log(this);
+        console.log('---------- /DUMP ubd');
+    },
+    // ----------------------------------------------------------------------
+    // public ACTIONS
+    // ----------------------------------------------------------------------
+
+    /**
+     * show rendered html content into set domid using the render function
+     * Optionally you can set a string to display an error message.
+     * 
+     * @param {string} sHtml  optional: htmlcode of an error message
+     */
+    this.render = function(sHtml) {
+        let out = sHtml ? sHtml : this._sRenderfunction(this._body);
+        this._oDomObject.innerHTML=out;
+    },
+
+    /**
+     * reset timer to update the content in dom id after reaching TTL
+     * used in setTtl
+     * 
+     * WIP: repeating the update braks out from current instance.
+     * But what works is 
+     *   var oUbd=new ubd(...)
+     *   by setting ttl = 0 and 
+     *   window.setInterval("oUbd.update();", 3000);
+     */
+    this.resetTimer = function(){
+        clearTimeout(this._oTimer);
+        // clearInterval(this._oTimer);
+        if(this._iTTL) {
+            let self = this;
+            self._oTimer=window.setTimeout(self.update, this._iTTL*1000);
+            // self._oTimer=window.setInterval(self.update, this._iTTL*1000);
+        }
+    },
+
+    /**
+     * make http request and call the renderer
+     */
+    this.update = async function(){
+        let self = this;
+        let url=( typeof this._sUrl2Fetch == "function" ) ? this._sUrl2Fetch() : this._sUrl2Fetch;
+        console.log("update from url [" + url + "]");
+        if (url == undefined){
+            console.error("SKIP update - there is no url in this object instance (anymore) :-/");
+            this.dumpme();
+            return 0;
+        }
+        try{
+            let response = await fetch(url, self._oHeader);
+            if (response.ok) {
+                self._body=await response.json();
+                
+                this.render();
+            } else {
+                this.render('<div class="app result1">'
+                    +'ERROR '+response.status+': '+response.statusText + ' - '
+                    +url
+                    +'</div>');
+            } 
+        } catch(e) {
+            this.render('<div class="app result1">'
+            +'UNKNOWN: no response from '
+            +this._sUrl2Fetch
+            +'</div>');
+            console.error(e);
+        }
+
+    }
+
+    // ----------------------------------------------------------------------
+    // MAIN
+    // ----------------------------------------------------------------------
+
+    if (arguments) {
+        this.init(arguments[0]);
+    }
+
+};
\ No newline at end of file
diff --git a/public_html/main.css b/public_html/main.css
index 341a7a0a2705c7ca33aef67df8409166e12d5a89..d07dc7917e6a7d93e0ba8ba1f0a45d1b91bcf40d 100644
--- a/public_html/main.css
+++ b/public_html/main.css
@@ -1,5 +1,3 @@
-
-
 :root{
     --color-0: #345;
     --color-h1: #c54;
@@ -21,20 +19,21 @@ body{
     font-family: verdana,arial;
 }
 
-button{ border: 1px solid  rgba(0,0,0,0.05); background: linear-gradient(#f8f8f8, #eee); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;}
-button:hover{ background: linear-gradient(#eee, #ddd); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;}
+button{ border: 1px solid  rgba(0,0,0,0.1); background: linear-gradient(#f8f8f8, #ddd); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;}
+button:hover{ background: linear-gradient(#eee, #ccc); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;}
 button:active{ border:1px solid #fc2;}
 input{ border:1px solid #ccc; padding: 0.4em;}
 
-footer{ position: fixed; bottom: 0; left: 0; width: 100%; padding: 1em; background: var(--bg-footer); text-align: center;}
-h1{color: var(--color-h1)}
-h2{color: var(--color-h2); margin-left: -1em;}
+footer{ position: fixed; bottom: 0; left: 0; width: 100%; padding: 1em; background: var(--bg-footer); border-top: 2px solid rgba(255,255,255,0.5); text-align: center;}
+h1{color: var(--color-h1); border-bottom: 1px solid; background: linear-gradient(#fff, #f0f4f8);}
+h2{color: var(--color-h2); margin-left: 0em;}
 
 h1>span, h2>span{font-size: 200%;}
 section{
     margin: 0 0 2em ;
-    padding: 0.2em 2em;
-    border-top: 0px solid #abc;
+    padding: 1em 2em;
+    border-top: 0px dashed #e0e4f0;
+    background: linear-gradient(10deg, #fff, #f0f4f8, #fff);
     border-radius: 1em;
 }
 
@@ -45,9 +44,10 @@ td{vertical-align: top;}
     border: 3px solid #bbb;
     border-radius: 1em;
     box-shadow: 0 0 3em rgba(0,0,0,0.2);
-    margin: 1em 5% 6em;
+    margin: 1em auto 6em;
     padding: 1em;
     min-height: 35em;
+    max-width: 80em;
 }
 
 .app{
@@ -62,9 +62,10 @@ td{vertical-align: top;}
 .app .title{font-weight: bold; font-size: 130%; cursor: pointer;}
 
 .app .url{font-size: 80%; font-weight: normal; margin-right: 2em;}
-.app .details{}
 
-.result0{background:#dfd    !important; background: linear-gradient(#dfd,#beb)!important;       color:#080}
+.result0{background:#dfd    !important; background: linear-gradient(#ded,#beb)!important;       color:#383}
 .result1{background:#eee    !important; background: linear-gradient(#eee,#ddd)!important;       color:#666;}
 .result2{background:#fff8d0 !important; background: linear-gradient(#fff0d0,#ffe0a0)!important; color:#870;}
 .result3{background:#fcd    !important; background: linear-gradient(#fcd,#faa)!important;       color:#800;}
+
+table.checks{border: 2px solid rgba(0,0,0,0.1);}