diff --git a/docker/.env b/docker/.env
index 31d8948d624acd82a12edd2a83bccaa1315cac0e..ecf872733b33c79e687f16cf9f38fcfb32f13d3e 100644
--- a/docker/.env
+++ b/docker/.env
@@ -1,6 +1,6 @@
 # ======================================================================
 #
-# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e
+# GENERATED BY init.sh - template: templates/dot_env - e2cde05722688ff85d3a93e9cd55787e
 # values to be used in docker-composer.yml
 #
 # ======================================================================
diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile
index 31263885c9f8b327a89409308bd918e7787a82f4..75b09744aa9c284855cb84e6cbe0340e3027fdb3 100644
--- a/docker/containers/web-server/Dockerfile
+++ b/docker/containers/web-server/Dockerfile
@@ -1,7 +1,7 @@
 #
-# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15
+# GENERATED BY init.sh - template: templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15
 #
-FROM php:8.2-apache
+FROM php:8.3-apache
 
 # install packages
 RUN apt-get update && apt-get install -y git unzip zip
diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
index 7ed4d6fc28954f0e749328a68bc927f268fa8b3c..77c28584763a0df629562da2ae1a68efdeaf4224 100644
--- a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
+++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
@@ -1,5 +1,5 @@
 #
-# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9
+# GENERATED BY init.sh - template: templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9
 #
 <VirtualHost *:80>
   DocumentRoot /var/www/my_new_app/public_html
diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini
index aa13bd779afa40bbfa25f10adef9baeae6d14f7d..8fc56969fb24b14a03349e4d16746d7607f67c2e 100644
--- a/docker/containers/web-server/php/extra-php-config.ini
+++ b/docker/containers/web-server/php/extra-php-config.ini
@@ -1,5 +1,5 @@
 ;
-; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2
+; GENERATED BY init.sh - template: templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2
 ;
 [PHP]
 
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 9b5d9940d606b8c37485cad39e0a4393efc06120..4fa133ff15feb1815a239591c1f718fa7cc5d21c 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,5 +1,5 @@
 #
-# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b
+# GENERATED BY init.sh - template: templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b
 #
 # ======================================================================
 #
@@ -19,7 +19,7 @@ services:
     build:
       context: .
       dockerfile: ./containers/web-server/Dockerfile
-    image: "php:8.2-apache"
+    image: "php:8.3-apache"
     container_name: 'my_new_app-server'
     ports:
       - '${APP_PORT}:80'
diff --git a/docker/init.sh b/docker/init.sh
index 060db24942ad10e1bb0f3abca96443d85f55d289..d75223516bd09e854db9378231f85b42b1ba4e70 100755
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -4,53 +4,271 @@
 # DOCKER PHP DEV ENVIRONMENT :: INIT
 #
 # ----------------------------------------------------------------------
-# 2021-11-nn  v1.0 <axel.hahn@iml.unibe.ch>
-# 2022-07-19  v1.1 <axel.hahn@iml.unibe.ch>  support multiple dirs for setfacl
-# 2022-11-16  v1.2 <www.axel-hahn.de>        use docker-compose -p "$APP_NAME"
-# 2022-12-18  v1.3 <www.axel-hahn.de>        add -p "$APP_NAME" in other docker commands
-# 2022-12-20  v1.4 <axel.hahn@unibe.ch>      replace fgrep with grep -F
-# 2023-03-06  v1.5 <www.axel-hahn.de>        up with and without --build
-# 2023-08-17  v1.6 <www.axel-hahn.de>        menu selection with single key (without return)
+# 2021-11-nn  v1.0  <axel.hahn@iml.unibe.ch>
+# 2022-07-19  v1.1  <axel.hahn@iml.unibe.ch>  support multiple dirs for setfacl
+# 2022-11-16  v1.2  <www.axel-hahn.de>        use docker-compose -p "$APP_NAME"
+# 2022-12-18  v1.3  <www.axel-hahn.de>        add -p "$APP_NAME" in other docker commands
+# 2022-12-20  v1.4  <axel.hahn@unibe.ch>      replace fgrep with grep -F
+# 2023-03-06  v1.5  <www.axel-hahn.de>        up with and without --build
+# 2023-08-17  v1.6  <www.axel-hahn.de>        menu selection with single key (without return)
+# 2023-11-10  v1.7  <axel.hahn@unibe.ch>      replace docker-compose with "docker compose"
+# 2023-11-13  v1.8  <axel.hahn@unibe.ch>      UNDO "docker compose"; update infos
+# 2023-11-15  v1.9  <axel.hahn@unibe.ch>      add help; execute multiple actions by params; new menu item: open app
+# 2023-12-07  v1.10 <www.axel-hahn.de>        simplyfy console command; add php linter
+# 2024-07-01  v1.11 <www.axel-hahn.de>        diff with colored output; suppress errors on port check
+# 2024-07-19  v1.12 <axel.hahn@unibe.ch>      apply shell fixes
+# 2024-07-22  v1.13 <axel.hahn@unibe.ch>      show info if there is no database container; speedup replacements
+# 2024-07-22  v1.14 <axel.hahn@unibe.ch>      show colored boxes with container status
+# 2024-07-24  v1.15 <axel.hahn@unibe.ch>      update menu output
+# 2024-07-26  v1.16 <axel.hahn@unibe.ch>      hide unnecessary menu items (WIP)
+# 2024-07-29  v1.17 <www.axel-hahn.de>        hide unnecessary menu items; reorder functions
+# 2024-08-14  v1.18 <www.axel-hahn.de>        update container view
+# 2024-09-20  v1.19 <www.axel-hahn.de>        detect dockerd-rootless (hides menu item to set permissions)
 # ======================================================================
 
-cd $( dirname $0 )
-. $( basename $0 ).cfg
+cd "$( dirname "$0" )" || exit 1
+
+# init used vars
+gittarget=
+frontendurl=
+
+_self=$( basename "$0" )
+
+# shellcheck source=/dev/null
+. "${_self}.cfg" || exit 1
+
+_version="1.19"
 
 # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git
 selfgitrepo="docker-php-starterkit.git"
 
-_version="1.6"
+fgGray="\e[1;30m"
+fgRed="\e[31m"
+fgGreen="\e[32m"
+fgBrown="\e[33m"
+fgBlue="\e[34m"
+
+fgInvert="\e[7m"
+fgReset="\e[0m"
+
+# ----- status varsiables
+# running containers
+DC_WEB_UP=0
+DC_DB_UP=0
+
+# repo of docker-php-starterkit is here?
+DC_REPO=1
+
+DC_CONFIG_CHANGED=0
+
+DC_WEB_URL=""
+
+isDockerRootless=0
+ps -ef | grep  dockerd-rootless | grep -q $USER && isDockerRootless=1
 
 # ----------------------------------------------------------------------
 # FUNCTIONS
 # ----------------------------------------------------------------------
 
+# ----------------------------------------------------------------------
+# STATUS FUNCTIONS
+
+# get container status and set global variable DC_REPO
+# DC_REPO = 0 nothing to do - repo was changed to project
+# DC_REPO = 1 if repo is in selfgitrepo (must be deleted)
+function _getStatus_repo(){
+    DC_REPO=0
+    git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo && DC_REPO=1
+}
+
+# check if any of the templates has a change that must be applied
+function _getStatus_template(){
+    _generateFiles "dryrun"
+}
+
+# get container status and set global variables
+# DC_WEB_UP - web container 
+# DC_DB_UP  - database container
+#   0 = down
+#   1 = up
+function _getStatus_docker(){
+    local _out
+    _out=$( docker-compose -p "$APP_NAME" ps)    
+
+    DC_WEB_UP=0
+    DC_DB_UP=0
+    grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1
+    grep -q "${APP_NAME}-db"     <<< "$_out"  && DC_DB_UP=1
+}
+
+function _getWebUrl(){
+    DC_WEB_URL="$frontendurl"
+    grep -q "${APP_NAME}-server" /etc/hosts && DC_WEB_URL="https://${APP_NAME}-server/"
+}
+
+# ----------------------------------------------------------------------
+# OUTPUT
+
 # draw a headline 2
 function h2(){
     echo
-    echo -e "\e[33m>>>>> $*\e[0m"
+    echo -e "$fgBrown>>>>> $*$fgReset"
 }
 
 # draw a headline 3
 function h3(){
     echo
-    echo -e "\e[34m----- $*\e[0m"
+    echo -e "$fgBlue----- $*$fgReset"
 }
 
-# function _gitinstall(){
-#     h2 "install/ update app from git repo ${gitrepo} in ${gittarget} ..."
-#     test -d ${gittarget} && ( cd ${gittarget}  && git pull )
-#     test -d ${gittarget} || git clone -b ${gitbranch} ${gitrepo} ${gittarget} 
-# }
+# helper for menu: print an inverted key
+function  _key(){
+    echo -en "\e[4;7m ${1} \e[0m"
+}
+
+# show menu in interactive mode and list keys in help with param -h
+# param  string  optional: set to "all" to show all menu items
+function showMenu(){
+
+    local _bAll=0
+    test -n "$1" && _bAll=1
+
+    local _spacer="    "
+
+    echo
+    if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo "${_spacer}$( _key g ) - remove git data of starterkit"
+        echo
+    fi
+
+    if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo "${_spacer}$( _key i ) - init application: set permissions"
+    fi
+
+    if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo "${_spacer}$( _key t ) - generate files from templates"
+    fi
+    if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+        echo "${_spacer}$( _key T ) - remove generated files"
+    fi
+    echo
+    if [ $DC_WEB_UP -eq 0 ] || [ $_bAll -eq 1 ]; then
+        if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+            echo "${_spacer}$( _key u ) - startup containers    docker-compose ... up -d"
+            echo "${_spacer}$( _key U ) - startup containers    docker-compose ... up -d --build"
+            echo
+            echo "${_spacer}$( _key r ) - remove containers     docker-compose rm -f"
+        fi
+    fi
+    if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo "${_spacer}$( _key s ) - shutdown containers   docker-compose stop"
+        echo
+        echo "${_spacer}$( _key m ) - more infos"
+        echo "${_spacer}$( _key o ) - open app [${APP_NAME}] $DC_WEB_URL"
+        echo "${_spacer}$( _key c ) - console (bash)"
+        echo "${_spacer}$( _key p ) - console check with php linter"
+    fi
+    echo
+    echo "${_spacer}$( _key q ) - quit"
+
+}
+function showHelp(){
+    cat <<EOH
+
+INITIALIZER FOR DOCKER APP v$_version
+
+A helper script written in Bash to bring up a PHP+Mysql application in docker.
+
+📄 Source : https://git-repo.iml.unibe.ch/iml-open-source/docker-php-starterkit
+📗 Docs   : https://os-docs.iml.unibe.ch/docker-php-starterkit/
+📜 License: GNU GPL 3.0
+(c) Institute for Medical Education; University of Bern
+
+
+SYNTAX:
+  $_self [-h|-v]
+  $_self [menu key [.. menu key N]]
+
+OPTIONS:
+  -h   show this help and exit
+  -v   show version exit
+
+MENU KEYS:
+  In the interactive menu are some keys to init an action.
+  The same keys can be put as parameter to start this action.
+  You can add multiples keys to apply multiple actions.
+
+$( showMenu "all" )
+
+EXAMPLES:
+
+  $_self           starts interactive mode
+  $_self u         bring up docker container(s) and stay in interactive mode
+  $_self i q       set write permissions and quit
+  $_self p q       start php linter and exit
+
+EOH
+}
+
+
+# show urls for app container
+function _showBrowserurl(){
+    echo "In a web browser open:"
+    echo "  $DC_WEB_URL"
+}
+
+# detect + show ports and urls for app container and db container
+function _showInfos(){
+    _showContainers long
+    h2 INFO
+
+    h3 "processes webserver"
+    # docker-compose top
+    docker top "${APP_NAME}-server"
+    if [ ! "$DB_ADD" = "false" ]; then
+        h3 "processes database"
+        docker top "${APP_NAME}-db"
+    fi
+
+    h3 "What to open in browser"
+    if echo >"/dev/tcp/localhost/${APP_PORT}"; then
+        # echo "OK, app port ${APP_PORT} is reachable"
+        # echo
+        _showBrowserurl
+    else
+        echo "ERROR: app port ${APP_PORT} is not available"
+    fi 2>/dev/null
+
+    if [ "$DB_ADD" != "false" ]; then
+        h3 "Check database port"
+        if echo >"/dev/tcp/localhost/${DB_PORT}"; then
+            echo "OK, db port ${DB_PORT} is reachable"
+            echo
+            echo "In a local DB admin tool you can connect it:"
+            echo "  host    : localhost"
+            echo "  port    : ${DB_PORT}"
+            echo "  user    : root"
+            echo "  password: ${MYSQL_ROOT_PASS}"
+        else
+            echo "NO, db port ${DB_PORT} is not available"
+        fi 2>/dev/null
+
+    fi
+    echo
+}
+
+# ----------------------------------------------------------------------
+# ACTIONS
 
 # set acl on local directory
 function _setWritepermissions(){
     h2 "set write permissions on ${gittarget} ..."
 
-    local _user=$( id -gn )
-    typeset -i local _user_uid=0
-    test -f /etc/subuid && _user_uid=$( grep $_user /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1
-    typeset -i local DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID
+    local _user; _user=$( id -gn )
+    local _user_uid; typeset -i _user_uid=0
+
+    test -f /etc/subuid && _user_uid=$( grep "$_user" /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1
+    local DOCKER_USER_OUTSIDE; typeset -i DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID
 
     set -vx
 
@@ -62,10 +280,10 @@ function _setWritepermissions(){
         sudo setfacl -bR "${mywritedir}"
 
         # default permissions: both the host user and the user with UID 33 (www-data on many systems) are owners with rwx perms
-        sudo setfacl -dRm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}"
+        sudo setfacl -dRm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}"
 
         # permissions: make both the host user and the user with UID 33 owner with rwx perms for all existing files/directories
-        sudo setfacl -Rm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}"
+        sudo setfacl -Rm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}"
     done
 
     set +vx
@@ -76,11 +294,10 @@ function _removeGitdata(){
     h2 "Remove git data of starterkit"
     echo -n "Current git remote url: "
     git config --get remote.origin.url
-    git config --get remote.origin.url 2>/dev/null | grep $selfgitrepo >/dev/null
-    if [ $? -eq 0 ]; then
+    if git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo; then
         echo
         echo -n "Delete local .git and .gitignore? [y/N] > "
-        read answer
+        read -r answer
         test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore )
     else
         echo "It was done already - $selfgitrepo was not found."
@@ -92,36 +309,64 @@ function _removeGitdata(){
 # see _generateFiles()
 function _fix_no-db(){
     local _file=$1
-    if [ $DB_ADD = false ]; then
-        typeset -i local iStart=$( cat ${_file} | grep -Fn "$CUTTER_NO_DATABASE" | cut -f 1 -d ':' )-1
+    if [ "$DB_ADD" = "false" ]; then
+        local iStart; typeset -i iStart
+        iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1
         if [ $iStart -gt 0 ]; then
-            sed -ni "1,${iStart}p" ${_file}
+            sed -ni "1,${iStart}p" "${_file}"
         fi
     fi
 }
 
+# helper function to generate replacements using sed
+# it loops over all vars in the config file
+# used in _generateFiles
+function _getreplaces(){
+    # loop over vars to make the replacement
+    grep "^[a-zA-Z]" "$_self.cfg" | while read -r line
+    do
+        # echo replacement: $line
+        mykey=$( echo "$line" | cut -f 1 -d '=' )
+        myvalue="$( eval echo \"\$"$mykey"\" )"
+
+        # TODO: multiline values fail here in replacement with sed 
+        echo -e "s#{{$mykey}}#${myvalue}#g"
+
+    done
+}
+
 # loop over all files in templates subdir make replacements and generate
 # a target file.
 # It skips if 
 #   - 1st line is not starting with "# TARGET: filename"
 #   - target file has no updated lines
+# If the 1st parameter is set to "dryrun" it will not generate files.
+# param string dryrun optional: set to "dryrun" to not generate files
 function _generateFiles(){
 
-    # re-read config vars
-    . $( basename $0 ).cfg
+    local _dryrun="$1"
+    DC_CONFIG_CHANGED=0
+
+    # shellcheck source=/dev/null
+    . "${_self}.cfg" || exit 1    
+
+    params=$( _getreplaces | while read -r line; do echo -n "-e '$line' ";  done )
 
     local _tmpfile=/tmp/newfilecontent$$.tmp
-    h2 "generate files from templates..."
-    for mytpl in $( ls -1 ./templates/* )
+    
+    test "$_dryrun" = "dryrun" || h2 "generate files from templates..."
+    for mytpl in templates/*
     do
         # h3 $mytpl
         local _doReplace=1
 
         # fetch traget file from first line
-        target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
+        target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
 
         if [ -z "$target" ]; then
-            echo SKIP: $mytpl - target was not found in 1st line
+            if [ "$_dryrun" != "dryrun" ]; then
+                echo "SKIP: $mytpl - target was not found in 1st line"
+            fi
             _doReplace=0
         fi
 
@@ -129,39 +374,37 @@ function _generateFiles(){
         if [ $_doReplace -eq 1 ]; then
 
             # write file from line 2 to a tmp file
-            sed -n '2,$p' $mytpl >$_tmpfile
+            sed -n '2,$p' "$mytpl" >"$_tmpfile"
 
             # add generator
             # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile
-            local _md5=$( md5sum $_tmpfile | awk '{ print $1 }' )
-            sed -i "s#{{generator}}#GENERATED BY $( basename $0 ) - template: $mytpl - $_md5#g" $_tmpfile
-
-            # loop over vars to make the replacement
-            grep "^[a-zA-Z]" $( basename $0 ).cfg | while read line
-            do
-                # echo replacement: $line
-                mykey=$( echo $line | cut -f 1 -d '=' )
-                myvalue="$( eval echo \"\${$mykey}\" )"
-                # grep "{{$mykey}}" $_tmpfile
-
-                # TODO: multiline values fail here in replacement with sed 
-                sed -i "s#{{$mykey}}#${myvalue}#g" $_tmpfile
-            done
+            local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' )
+            sed -i "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile
+
+            # apply all replacements to the tmp file
+            eval sed -i "$params" "$_tmpfile" || exit
+
             _fix_no-db $_tmpfile
 
             # echo "changes for $target:"
-            diff  "../$target"  "$_tmpfile" | grep -v "$_md5" | grep -v "^---" | grep .
-            if [ $? -eq 0 -o ! -f "../$target" ]; then
-                echo -n "$mytpl - changes detected - writing [$target] ... "
-                mkdir -p $( dirname  "../$target" ) || exit 2
-                mv "$_tmpfile" "../$target" || exit 2
-                echo OK
+            if diff --color=always "../$target"  "$_tmpfile" 2>/dev/null | grep -v "$_md5" | grep -v "^---" | grep . || [ ! -f "../$target" ]; then
+                if [ "$_dryrun" = "dryrun" ]
+                then
+                    DC_CONFIG_CHANGED=1
+                else
+                    echo -n "$mytpl - changes detected - writing [$target] ... "
+                    mkdir -p "$( dirname  ../"$target" )" || exit 2
+                    mv "$_tmpfile" "../$target" || exit 2
+                    echo OK
+                    echo
+                fi
             else
                 rm -f $_tmpfile
-                echo "SKIP: $mytpl - Nothing to do."
+                if [ "$_dryrun" != "dryrun" ]; then
+                    echo "SKIP: $mytpl - Nothing to do."
+                fi
             fi
         fi
-        echo
     done
 
 }
@@ -170,104 +413,116 @@ function _generateFiles(){
 # a traget file.
 function _removeGeneratedFiles(){
     h2 "remove generated files..."
-    for mytpl in $( ls -1 ./templates/* )
+    for mytpl in templates/*
     do
-        h3 $mytpl
+        h3 "$mytpl"
 
         # fetch traget file from first line
-        target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
+        target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
 
-        if [ ! -z "$target" -a -f "../$target" ]; then
+        if [ -n "$target" ] && [ -f "../$target" ]; then
             echo -n "REMOVING "
             ls -l "../$target" || exit 2
             rm -f "../$target" || exit 2
             echo OK
         else
-            echo SKIP: $target
+            echo "SKIP: $target"
         fi
         
     done
 }
 
+
+# show running containers
 function _showContainers(){
     local bLong=$1
-    h2 CONTAINERS
-    if [ -z "$bLong" ]; then
-        docker-compose -p "$APP_NAME" ps
-    else
-        docker ps | grep $APP_NAME
+
+    local _out
+
+    local sUp=".. UP"
+    local sDown=".. down"
+
+    local Status=
+    local StatusWeb="$sDown"
+    local StatusDb="$sDown"
+    local colWeb=
+    local colDb=
+
+    colDb="$fgRed"
+    colWeb="$fgRed"
+
+    if [ $DC_WEB_UP -eq 1 ]; then
+        colWeb="$fgGreen"
+        StatusWeb="$sUp"
+    fi
+    
+    if [ $DC_DB_UP -eq 1 ]; then
+        colDb="$fgGreen"
+        StatusDb="$sUp"
     fi
-}
 
+    if [ "$DB_ADD" = "false" ]; then
+        colDb="$fgGray"
+        local StatusDb=".. N/A"
+        Status="This app has no database container."
+    fi
 
-# a bit stupid ... i think I need to delete it.
-function _showInfos(){
-    _showContainers long
-    h2 INFO
+    h2 CONTAINERS
 
-    h3 "processes"
-    docker-compose top
+    echo
+    printf "  $colWeb$fgInvert  %-32s  $fgReset   $colDb$fgInvert  %-32s  $fgReset\n"      "WEB ${StatusWeb}"  "DB ${StatusDb}"
+    printf "    %-32s  $fgReset     %-32s  $fgReset\n"      "PHP ${APP_PHP_VERSION}"      "${MYSQL_IMAGE}"
+    printf "    %-32s  $fgReset     %-32s  $fgReset\n"      ":${APP_PORT}"                ":${DB_PORT}"
+
+    echo
 
-    h3 "Check app port"
-    >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && (
-        echo "OK, app port ${APP_PORT} is reachable"
+    if [ -n "$Status" ]; then
+        echo "  $Status"
         echo
-        echo "In a web browser open:"
-        echo "  $frontendurl"
-    )
-    h3 "Check database port"
-    >/dev/tcp/localhost/${DB_PORT} 2>/dev/null && (
-        echo "OK, db port ${DB_PORT} is reachable"
+    fi
+
+    if [ -n "$bLong" ]; then
+        echo "$_out"
+
+        h2 STATS
+        docker stats --no-stream
         echo
-        echo "In a local DB admin tool:"
-        echo "  host    : localhost"
-        echo "  port    : ${DB_PORT}"
-        echo "  user    : root"
-        echo "  password: ${MYSQL_ROOT_PASS}"
-    )
-    echo
-}
+    fi
 
-# helper for menu: print an inverted key
-function  _key(){
-    printf "\e[4;7m ${1} \e[0m"
 }
 
 # helper: wait for a return key
 function _wait(){
-    echo -n "... press RETURN > "; read -r
+    local _wait=15
+    echo -n "... press RETURN ... or wait $_wait sec > "; read -r -t $_wait
+    echo
 }
 
 # ----------------------------------------------------------------------
 # MAIN
 # ----------------------------------------------------------------------
 
-action=$1
+action=$1; shift 1
 
 while true; do
-    echo
-    echo -e "\e[32m===== INITIALIZER FOR DOCKER APP [$APP_NAME] v$_version ===== \e[0m\n\r"
 
-    if [ -z "$action" ]; then
+    _getStatus_repo
+    _getStatus_docker
+    _getStatus_template
+    _getWebUrl
 
-        _showContainers
+    if [ -z "$action" ]; then
 
-        h2 MENU
-        echo "  $( _key g ) - remove git data of starterkit"
+        echo "_______________________________________________________________________________"
         echo
-        echo "  $( _key i ) - init application: set permissions"
-        echo "  $( _key t ) - generate files from templates"
-        echo "  $( _key T ) - remove generated files"
+        printf "  %-70s ______\n" "${APP_NAME^^}  ::  Initializer for docker"
+        echo "________________________________________________________________________/ $_version"
         echo
-        echo "  $( _key u ) - startup containers    docker-compose ... up -d"
-        echo "  $( _key U ) - startup containers    docker-compose ... up -d --build"
-        echo "  $( _key s ) - shutdown containers   docker-compose stop"
-        echo "  $( _key r ) - remove containers     docker-compose rm -f"
-        echo
-        echo "  $( _key m ) - more infos"
-        echo "  $( _key c ) - console (bash)"
-        echo
-        echo "  $( _key q ) - quit"
+
+        _showContainers
+
+        h2 MENU       
+        showMenu
         echo
         echo -n "  select >"
         read -rn 1 action 
@@ -275,6 +530,8 @@ while true; do
     fi
 
     case "$action" in
+        "-h") showHelp; exit 0 ;;
+        "-v") echo "$_self $_version"; exit 0 ;;
         g)
             _removeGitdata
             ;;
@@ -289,51 +546,84 @@ while true; do
             _removeGeneratedFiles
             rm -rf containers
             ;;
-        # not in the menu
-        # f)
-        #     _removeGeneratedFiles
-        #     _generateFiles
-        #     _wait
-        #     ;;
         m)
             _showInfos
             _wait
             ;;
         u|U)
-            dockerUp="docker-compose -p "$APP_NAME" --verbose up -d --remove-orphans"
+            h2 "Bring up..."
+            dockerUp="docker-compose -p $APP_NAME --verbose up -d --remove-orphans"
             if [ "$action" = "U" ]; then
                 dockerUp+=" --build"
             fi
+            echo "$dockerUp"
             if $dockerUp; then
-                echo "In a web browser:"
-                echo "  $frontendurl"
+                _showBrowserurl
             else
                 echo "ERROR: docker-compose up failed :-/"
                 docker-compose -p "$APP_NAME" logs | tail
             fi
             echo
 
-            _wait
             ;;
         s)
+            h2 "Stopping..."
             docker-compose -p "$APP_NAME" stop
             ;;
         r)
+            h2 "Removing..."
             docker-compose -p "$APP_NAME" rm -f
             ;;
         c)
-            docker ps
-            echo -n "id or name >"
-            read dockerid
-            test -z "$dockerid" || docker exec -it $dockerid /bin/bash
+            h2 "Console"
+            _containers=$( docker-compose -p "$APP_NAME" ps | sed -n "2,\$p" | awk '{ print $1}' )
+            if [ "$DB_ADD" = "false" ]; then
+                dockerid=$_containers
+            else
+                echo "Select a container:"
+                sed "s#^#    #g" <<< "$_containers"
+                echo -n "id or name >"
+                read -r dockerid
+            fi
+            test -z "$dockerid" || (
+                echo
+                echo "> docker exec -it $dockerid /bin/bash     (type 'exit' + Return when finished)"
+                docker exec -it "$dockerid" /bin/bash
+            )
+            ;;
+        p)
+            h2 "PHP $APP_PHP_VERSION linter"
+
+            dockerid="${APP_NAME}-server"
+            echo -n "Scanning ... "
+            typeset -i _iFiles
+            _iFiles=$( docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' " | wc -l )
+
+            if [ $_iFiles -gt 0 ]; then
+                echo "found $_iFiles [*.php] files ... errors from PHP $APP_PHP_VERSION linter:"
+                time if echo "$APP_PHP_VERSION" | grep -E "([567]\.|8\.[012])" >/dev/null ; then
+                    docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' -exec php -l {} \; | grep -v '^No syntax errors detected'"
+                else
+                    docker exec -it "$dockerid" /bin/bash -c "php -l \$( find . -name '*.php' ) | grep -v '^No syntax errors detected' "
+                fi
+                echo
+                _wait
+            else
+                echo "Start your docker container first."
+            fi
+            ;;
+        o) 
+            h2 "Open app ..."
+            xdg-open "$DC_WEB_URL"
             ;;
         q)
+            h2 "Bye!"
             exit 0;
             ;;
         *) 
             test -n "$action" && ( echo "  ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 )
     esac
-    action=
+    action=$1; shift 1
 done
 
 
diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg
index 7c781c5f4288fe3acf990cd7ad6db2241f78a9bd..e30c69e0a9664ebff0532f299a6af60826959057 100644
--- a/docker/init.sh.cfg
+++ b/docker/init.sh.cfg
@@ -17,7 +17,7 @@ APP_APT_PACKAGES="git unzip zip"
 #APP_APACHE_MODULES="rewrite"
 APP_APACHE_MODULES=""
 
-APP_PHP_VERSION=8.2
+APP_PHP_VERSION=8.3
 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug"
 # APP_PHP_MODULES="curl mbstring xml zip xdebug"
 APP_PHP_MODULES="xdebug"
diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md"
index 99773cfd6df1ecfd8aa573663ab5c07fa1af4797..372e2f38a6168e613b1b932853b13353859d53a6 100644
--- "a/docs/10_\360\237\223\221_Description.md"
+++ "b/docs/10_\360\237\223\221_Description.md"
@@ -1,6 +1,6 @@
 ## Requirements
 
-* PHP 7+ (up to PHP 8.2)
+* PHP 8 (up to PHP 8.3)
 * Webserver (docs describe usage for Apache httpd)
 
 ## Features
diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php
index 7513e18e30ed4e2157056a8b230c3cb51c91f146..d11fbe0ffcca843ff61d96ecb6247703e3833291 100644
--- a/public_html/classes/redirect.admin.class.php
+++ b/public_html/classes/redirect.admin.class.php
@@ -18,6 +18,7 @@ require_once 'redirect.class.php';
  * 2022-05-23  v1.6  ah  add http head check+render output; 
  * 2022-05-31  v1.7  ah  optical changes
  * 2023-08-28  v1.8  ah  remove php warning if there is no config yet
+ * 2024-10-03  v1.9  ah  php8 only: typed variables
  */
 
 /**
@@ -25,39 +26,46 @@ require_once 'redirect.class.php';
  *
  * @author axel
  */
-class redirectadmin extends redirect {
+class redirectadmin extends redirect
+{
 
-    protected function _getCurlOptions(){
-        $aReturn=array(
+    /**
+     * Get default curl options
+     * @return array
+     */
+    protected function _getCurlOptions(): array
+    {
+        $aReturn = [
             CURLOPT_HEADER => true,
             CURLOPT_FOLLOWLOCATION => true,
             CURLOPT_RETURNTRANSFER => true,
             CURLOPT_USERAGENT => strip_tags($this->sAbout),
             CURLOPT_VERBOSE => false,
             CURLOPT_ENCODING => 'gzip, deflate',  // to fetch encoding
-            CURLOPT_HTTPHEADER => array(
+            CURLOPT_HTTPHEADER => [
                 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                 'Accept-Language: en',
                 'DNT: 1',
-            ),
+            ],
 
             // TODO: this is unsafe .. better: let the user configure it
             CURLOPT_SSL_VERIFYHOST => false,
             CURLOPT_SSL_VERIFYPEER => false,
             CURLOPT_TIMEOUT => 5,
-        );
+        ];
         return $aReturn;
     }
 
     /**
-     * make a single http(s) get request and return the response body
+     * Make a single http(s) get request and return the response body
      * @param string   $url          url to fetch
      * @param boolean  $bHeaderOnly  optional: true=make HEAD request; default: false (=GET)
      * @return string
      */
-    public function httpGet($url, $bHeaderOnly = false) {
+    public function httpGet(string $url, bool $bHeaderOnly = false): bool|string
+    {
         $ch = curl_init($url);
-        foreach ($this->_getCurlOptions() as $sCurlOption=>$sCurlValue){
+        foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) {
             curl_setopt($ch, $sCurlOption, $sCurlValue);
         }
         if ($bHeaderOnly) {
@@ -69,113 +77,124 @@ class redirectadmin extends redirect {
         return ($res);
     }
 
-    public function renderHttpResponseHeader($sHeader){
-        $sReturn=$sHeader;
-        if(!$sReturn){
-            $sReturn='<pre><span class="status status-error">Request failed. </span><br>'
-                .'No data... no response.<br>'
-                .'Maybe ... '
-                .'<ul>'
-                    .'<li>the nostname does not exist ... </li>'
-                    .'<li>or there is a network problem ... or </li>'
-                    .'<li>the webservice on the target system does not run.</li>'
-                .'</ul>'
-            .'</pre>'
+    /**
+     * Get html code for a response header of a request
+     * 
+     * @param string $sHeader
+     * @return string
+     */
+    public function renderHttpResponseHeader(string $sHeader): string
+    {
+        $sReturn = $sHeader;
+        if (!$sReturn) {
+            $sReturn = '<pre><span class="status status-error">Request failed. </span><br>'
+                . 'No data... no response.<br>'
+                . 'Maybe ... '
+                . '<ul>'
+                . '<li>the nostname does not exist ... </li>'
+                . '<li>or there is a network problem ... or </li>'
+                . '<li>the webservice on the target system does not run.</li>'
+                . '</ul>'
+                . '</pre>'
             ;
         } else {
 
-            $sReturn=preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn);
-            $sReturn=preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn);
-            $sReturn=preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn);
-            $sReturn=preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn);
-            $sReturn=preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn);
-            $sReturn=preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn);
+            $sReturn = preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn);
+            $sReturn = preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn);
+            $sReturn = preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn);
+            $sReturn = preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn);
+            $sReturn = preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn);
+            $sReturn = preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn);
 
-            $sReturn=preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn);
-            $sReturn=preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn);
-            $sReturn.='</pre>';
+            $sReturn = preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn);
+            $sReturn = preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn);
+            $sReturn .= '</pre>';
 
             preg_match_all('/(HTTP\/.*)/i', $sReturn, $aTmp);
             // $sReturn.=print_r($aTmp, 1);
-            $iHops=(count($aTmp[0])-1);
+            $iHops = (count($aTmp[0]) - 1);
 
-            $sReturn=($iHops>0
-                ? 'Found hops: <strong>'.$iHops.'</strong>'
-                    .($iHops>1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '' ).'<br><br>'
+            $sReturn = ($iHops > 0
+                ? 'Found hops: <strong>' . $iHops . '</strong>'
+                . ($iHops > 1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '') . '<br><br>'
                 : ''
-                ).$sReturn
-                ;
+            ) . $sReturn
+            ;
         }
 
         return $sReturn;
     }
 
     /**
-     * check if admin is enabled
+     * Check if admin is enabled
+     * 
      * @return bool
      */
-    public function isEnabled(){
-        $sFile2Enable=__DIR__ . '/'.basename(__FILE__).'_enabled.txt';
+    public function isEnabled(): bool
+    {
+        $sFile2Enable = __DIR__ . '/' . basename(__FILE__) . '_enabled.txt';
         return file_exists($sFile2Enable);
     }
 
     /**
-     * get ip address of a given hostname as ip (string) or false
+     * Get ip address of a given hostname as ip (string) or false
      * @return string|bool
      */
-    protected function _getIp($sHostname){
-        $sIp=gethostbyname($sHostname);
-        return $sIp===$sHostname ? false : $sIp;
+    protected function _getIp(string $sHostname): string
+    {
+        $sIp = gethostbyname($sHostname);
+        return $sIp === $sHostname ? '' : $sIp;
     }
 
     /**
-     * get an array with all config entries in all json files
+     * Get an array with all config entries in all json files
      * including some error checking
+     * 
      * @return array
      */
-    public function getHosts(){
-        $aReturn = array();
-        $aErrors = array();
-        foreach(glob($this->sConfigDir . '/redirects_*.json') as $sFilename){
-            $sMyHost= str_replace(array('redirects_', '.json'), array('',''), basename($sFilename));
-            $aReturn[$sMyHost]=array(
-                'type'=>'config',
-                'file'=>$sFilename,
-                'ip'=> $this->_getIp($sMyHost),
-                'aliases'=>array(),
-                'redirects'=>json_decode(file_get_contents($sFilename), 1),
-            );
-            if (!$aReturn[$sMyHost]['ip']){
-                $aErrors[]=basename($sFilename).': The hostname was not found in DNS: '.$sMyHost;
+    public function getHosts(): array
+    {
+        $aReturn = [];
+        $aErrors = [];
+        foreach (glob($this->sConfigDir . '/redirects_*.json') as $sFilename) {
+            $sMyHost = str_replace(['redirects_', '.json'], ['', ''], basename($sFilename));
+            $aReturn[$sMyHost] = [
+                'type' => 'config',
+                'file' => $sFilename,
+                'ip' => $this->_getIp($sMyHost),
+                'aliases' => [],
+                'redirects' => json_decode(file_get_contents($sFilename), 1),
+            ];
+            if (!$aReturn[$sMyHost]['ip']) {
+                $aErrors[] = basename($sFilename) . ': The hostname was not found in DNS: ' . $sMyHost;
             }
         }
-        $aAliases=$this->_getAliases();
-        if(is_array($aAliases) && count($aAliases)){
-            foreach($aAliases as $sAlias=>$sConfig){
-                if(isset($aReturn[$sAlias])){
-                    $aErrors[]="alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority).";
+        $aAliases = $this->_getAliases();
+        if (is_array($aAliases) && count($aAliases)) {
+            foreach ($aAliases as $sAlias => $sConfig) {
+                if (isset($aReturn[$sAlias])) {
+                    $aErrors[] = "alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority).";
                 } else {
-                    if(!isset($aReturn[$sConfig])){
-                        $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist.";
+                    if (!isset($aReturn[$sConfig])) {
+                        $aErrors[] = "alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist.";
                     } else {
-                        $aReturn[$sConfig]['aliases'][]=$sAlias;
-                        $aReturn[$sAlias]=array(
-                            'type'=>'alias',
-                            'target'=>$sConfig,
-                            'ip'=> $this->_getIp($sAlias),
-                        );
-                        if (!$aReturn[$sAlias]['ip']){
-                            $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias;
+                        $aReturn[$sConfig]['aliases'][] = $sAlias;
+                        $aReturn[$sAlias] = [
+                            'type' => 'alias',
+                            'target' => $sConfig,
+                            'ip' => $this->_getIp($sAlias),
+                        ];
+                        if (!$aReturn[$sAlias]['ip']) {
+                            $aErrors[] = 'alias.json: The hostname was not found in DNS: ' . $sAlias;
                         }
                     }
                 }
             }
         }
-        $aReturn['_errors']=$aErrors;
+        $aReturn['_errors'] = $aErrors;
         ksort($aReturn);
         return $aReturn;
     }
 
-
 }
 
diff --git a/public_html/classes/redirect.class.php b/public_html/classes/redirect.class.php
index c04108a2279046efa59d944753aa28b4d45c725b..168151820d4dfb956b28d09f83b9ced86a023f86 100644
--- a/public_html/classes/redirect.class.php
+++ b/public_html/classes/redirect.class.php
@@ -24,6 +24,7 @@
  * 2020-05-06  v1.3  ah  added aliases for multiple domains with the same config
  * 2020-05-06  v1.4  ah  rewrite as class
  * 2023-08-28  v1.5  ah  fix loop over config with missing regex section.
+ * 2024-10-03  v1.6  ah  php8 only: typed variables
  */
 
 /**
@@ -31,86 +32,156 @@
  *
  * @author axel
  */
-class redirect {
+class redirect
+{
 
     // ----------------------------------------------------------------------
     // CONFIG
     // ----------------------------------------------------------------------
 
-    protected $bDebug = false;
-    protected $sConfigDir = __DIR__ . '/../../config';
-    protected $sAbout = 'IML redirect <small>v1.4</small>';
+    /**
+     * Flag: debug is enabled?
+     * @var bool
+     */
+    protected bool $bDebug = false;
+
+    /**
+     * configuration dir
+     * @var string
+     */
+    protected string $sConfigDir = __DIR__ . '/../../config';
 
-    protected $sHostname = false;
-    protected $sRequest = false;
+    /**
+     * About message
+     * @var string
+     */
+    protected string $sAbout = 'IML redirect <small>v1.4</small>';
 
-    protected $sCfgfile = false;
-    protected $aConfig  = false;
-    protected $aRedirect  = false;
-    
-    public $urlRepo='https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler';
-    public $urlDocs='https://os-docs.iml.unibe.ch/redirect-handler/';
+    /**
+     * Hostname
+     * @var string
+     */
+    protected string $sHostname = '';
+
+    /**
+     * Request to handle and to redirect to aconfigured target url
+     * @var string
+     */
+    protected string $sRequest = '';
+
+    /**
+     * Config file
+     * @var 
+     */
+    protected string $sCfgfile = '';
+
+    /**
+     * Config data for the current hostname
+     * @var 
+     */
+    protected array $aConfig = [];
+
+    /**
+     * Redirect data for the current request
+     * @var array
+     */
+    protected array $aRedirect = [];
+
+    /**
+     * URL to the repository of this project
+     * @var string
+     */
+    public string $urlRepo = 'https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler';
+
+    /**
+     * Url to the docs
+     * @var string
+     */
+    public string $urlDocs = 'https://os-docs.iml.unibe.ch/redirect-handler/';
 
     // ----------------------------------------------------------------------
     // CONSTRUCTOR
     // ----------------------------------------------------------------------
-    public function __constructor($sHostname=false, $sRequest=false) {
-        if($sHostname){
+
+    /**
+     * Constructor
+     * @param string $sHostname  hostname
+     * @param string $sRequest   request to proces
+     * @return void
+     */
+    public function __constructor(string $sHostname = '', string $sRequest = '')
+    {
+        if ($sHostname) {
             $this->setHost($sHostname);
         }
-        if($sRequest){
+        if ($sRequest) {
             $this->setRequest($sRequest);
         }
-        
-        return true;
+
     }
 
-    
+
     // ----------------------------------------------------------------------
     // DEBUG
     // ----------------------------------------------------------------------
-    
+
     /**
-     * write a debug message into the header
+     * Write a debug message into the http response header
+     * 
      * @global    boolean $bDebug         flag: show debug infos?
      * @staticvar int     $i              counter
+     * 
      * @param     string  $sDebugMessage  message to show
      * @return boolean
      */
-    protected function _wd($sDebugMessage) {
-        
+    protected function _wd(string $sDebugMessage): bool
+    {
+
         if (!$this->bDebug) {
             return false;
         }
         static $i;
         $i++;
-        header('X-DEBUG-' . $i . ': ' . $sDebugMessage);
+        header("X-DEBUG-$i: $sDebugMessage");
         return true;
     }
+
     // ----------------------------------------------------------------------
     // SET INTERNAL VARS
     // ----------------------------------------------------------------------
+
     /**
-     * get a string with full path of a config file with aliases
+     * Get a string with full path of a config file with aliases
      * @return string
      */
-    protected function _generateAliasfile() {
+    protected function _generateAliasfile(): string
+    {
         return $this->sConfigDir . '/aliases.json';
     }
+
     /**
-     * get a string with full path of a config file based on hostname
-     * @param string $sHostname
+     * Get a string with full path of a config file based on hostname
+     * 
+     * @param string $sHostname  hostname/ fqdn
      * @return string
      */
-    protected function _generateCfgfile($sHostname) {
-        
+    protected function _generateCfgfile(string $sHostname): string
+    {
+
         return $this->sConfigDir . '/redirects_' . $sHostname . '.json';
     }
-    
-    protected function _getAliases(){
-        $sAliasfile=$this->_generateAliasfile();
-        $this->_wd('check alias file '.$sAliasfile);
-        if(!file_exists($sAliasfile)){
+
+    /**
+     * Get Aliases from aliases.json
+     * It returns false if no alias was found
+     * 
+     * @return bool|array
+     */
+    protected function _getAliases(): bool|array
+    {
+        $sAliasfile = $this->_generateAliasfile();
+        $this->_wd('check alias file ' . $sAliasfile);
+        if (!file_exists($sAliasfile)) {
             $this->_wd('alias do not exist');
             // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01');
             return false;
@@ -120,49 +191,51 @@ class redirect {
     }
 
     /**
-     * get an array with redirect config based on a given hostname
-     * @param string  $sHostname  hostname
-     * @param boolean $bAbort     flag: true = do not scan aliases (used for loop detection)
-     * @return array
+     * Get an array with redirect config based on a given hostname.
+     * @return bool
      */
-    protected function _getConfig() {
-        $this->aConfig=false;
-        $this->aRedirect=false;
+    protected function _getConfig(): bool
+    {
+        $this->aConfig = [];
+        $this->aRedirect = [];
         $this->_getEffectiveConfigfile();
-        if($this->sCfgfile){
-            $aConfig=json_decode(file_get_contents($this->sCfgfile), 1);
+        if ($this->sCfgfile) {
+            $aConfig = json_decode(file_get_contents($this->sCfgfile), 1);
             if (!is_array($aConfig) || !count($aConfig)) {
                 $this->_wd('no config available');
                 // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 02');
+            } else {
+                $this->aConfig = $aConfig;
             }
-            $this->aConfig=$aConfig;
         }
         return true;
     }
-    
+
     /**
-     * get an array with redirect config based on a given hostname
+     * Get filename of config file based on a given hostname
      * or detection in the alias config
+     * 
      * @param string  $sHostname  hostname
      * @param boolean $bAbort     flag: true = do not scan aliases (used for loop detection)
-     * @return array
+     * @return bool|string
      */
-    protected function _getEffectiveConfigfile($sHostname=false, $bAbort=false) {
-        if(!$sHostname){
-            $sHostname=$this->sHostname;
+    protected function _getEffectiveConfigfile(string $sHostname = '', bool $bAbort = false): bool|string
+    {
+        if (!$sHostname) {
+            $sHostname = $this->sHostname;
         }
-        $this->sCfgfile=false;
+        $this->sCfgfile = false;
         $sCfgfile = $this->_generateCfgfile($sHostname);
-        $this->_wd('check config '.$sCfgfile);
+        $this->_wd('check config ' . $sCfgfile);
         if (!file_exists($sCfgfile)) {
-            if($bAbort){
+            if ($bAbort) {
                 // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01');
                 return false;
             }
             $aAliases = $this->_getAliases();
-            $this->_wd('checking aliases '.print_r($aAliases,1));
-            if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))){
-                $this->_wd('sorry no valid alias for '. $sHostname);
+            $this->_wd('checking aliases ' . print_r($aAliases, 1));
+            if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))) {
+                $this->_wd('sorry no valid alias for ' . $sHostname);
                 // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01');
                 return false;
             }
@@ -170,39 +243,42 @@ class redirect {
             return $this->_getEffectiveConfigfile($aAliases[$this->sHostname], 1);
 
         }
-        $this->sCfgfile=$sCfgfile;
+        $this->sCfgfile = $sCfgfile;
         return $sCfgfile;
     }
-    
-    
+
+
     /**
-     * enable/ disable debug
+     * Enable/ disable debug
      * @param boolean $bEnable
      */
-    public function setDebug($bEnable){
-        $this->bDebug=!!$bEnable;
+    public function setDebug(bool $bEnable): bool
+    {
+        $this->bDebug = !!$bEnable;
         return true;
     }
-    
+
     /**
-     * set hostname; internally it detects the config too
-     * @param type $sHostname
+     * Set hostname; internally it detects the config too
+     * @param string $sHostname
      * @return boolean
      */
-    public function setHost($sHostname){
-        $this->sHostname=$sHostname;
+    public function setHost(string $sHostname): bool
+    {
+        $this->sHostname = $sHostname;
         $this->_getConfig();
         return true;
     }
-    
+
     /**
-     * set the request
-     * @param type $sRequest
+     * Set the request
+     * @param string $sRequest
      * @return boolean
      */
-    public function setRequest($sRequest){
-        $this->sRequest=$sRequest;
-        $this->aRedirect=false;
+    public function setRequest(string $sRequest): bool
+    {
+        $this->sRequest = $sRequest;
+        $this->aRedirect = false;
         return true;
     }
 
@@ -211,26 +287,28 @@ class redirect {
     // ----------------------------------------------------------------------
 
     /**
-     * get an array with the matching redirect; it returns false if none was 
+     * Get an array with the matching redirect; it returns false if none was 
      * detected
+     * 
      * @return array
      */
-    public function getRedirect(){
-        if(is_array($this->aRedirect)){
+    public function getRedirect(): array
+    {
+        if (is_array($this->aRedirect)) {
             return $this->aRedirect;
         }
-        
-        $aRedirect = false;
-        
+
+        $aRedirect = [];
+
         // remark:
         // $this->aConfig is set in setHost() with $this->_getConfig();
-        
+
         if (isset($this->aConfig['direct'][$this->sRequest])) {
             $this->_wd("DIRECT MATCH");
             $aRedirect = $this->aConfig['direct'][$this->sRequest];
         } else {
             $this->_wd("no direct match ... scanning regex");
-            if(isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])){
+            if (isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])) {
                 foreach (array_keys($this->aConfig['regex']) as $sRegex) {
                     $this->_wd("check if regex [$sRegex] matches $this->sRequest");
                     if (preg_match('#' . $sRegex . '#', $this->sRequest)) {
@@ -241,21 +319,32 @@ class redirect {
                 }
             }
         }
-        $this->aRedirect=$aRedirect;
+        $this->aRedirect = $aRedirect;
         return $aRedirect;
     }
-    
-    public function getRedirectCode($iNone=false){
-        $aRedirect=$this->getRedirect();
+
+    /**
+     * Get 30x redirect code
+     * @param integer $iNone  fallback value if no redirect status code was found
+     * @return integer
+     */
+    public function getRedirectCode($iNone = 307): int
+    {
+        $aRedirect = $this->getRedirect();
         return isset($aRedirect['code']) ? $aRedirect['code'] : $iNone;
     }
-    public function getRedirectTarget(){
-        $aRedirect=$this->getRedirect();
-        return isset($aRedirect['target']) ? $aRedirect['target'] : false;
+
+    /**
+     * Get Target url of redirect
+     * @return string
+     */
+    public function getRedirectTarget(): string
+    {
+        $aRedirect = $this->getRedirect();
+        return $aRedirect['target'] ?? '';
     }
-    
 
-    
+
     // ----------------------------------------------------------------------
     // FUNCTIONS - SEND DATA
     // ----------------------------------------------------------------------
@@ -264,27 +353,29 @@ class redirect {
      * make the redirect if it was found ... or a 404
      * @return true
      */
-    public function makeRedirect(){
-        $sTarget=$this->getRedirectTarget();
-        if(!$sTarget){
+    public function makeRedirect(): bool
+    {
+        $sTarget = $this->getRedirectTarget();
+        if (!$sTarget) {
             $this->_wd('send a not found');
             $this->sendBody(404, '<h1>404 Not found</h1>');
         } else {
-            $iCode=$this->getRedirectCode();
-            $this->_wd("Redirect with http status [" . $iCode . "] to new Location: " . $sTarget);
+            $iCode = $this->getRedirectCode();
+            $this->_wd("Redirect with http status [$iCode] to new Location: $sTarget");
             $this->sendHttpStatusheader($iCode);
             header("Location: " . $sTarget);
         }
         return true;
     }
-    
+
     /**
      * send http status header
      * @param integer $iCode http code
      * @return boolean
      */
-    public function sendHttpStatusheader($iCode) {
-        $aHeaders = array(
+    public function sendHttpStatusheader(int $iCode): bool
+    {
+        $aHeaders = [
             301 => 'Moved Permanently',
             302 => 'Found', // (Moved Temporarily) 
             303 => 'See other', // redirect with GET
@@ -293,7 +384,7 @@ class redirect {
             404 => 'Not found',
             410 => 'Gone',
             500 => 'Internal Server Error',
-        );
+        ];
 
         $iCode = (int) $iCode;
         if (!isset($aHeaders[$iCode])) {
@@ -308,28 +399,29 @@ class redirect {
      * @see sendHttpStatusheader()
      * 
      * @param integer  $iCode  http status code
-     * @param stringg  $sBody  message text as html code
+     * @param string   $sBody  message text as html code
+     * @return void
      */
-    public function sendBody($iCode, $sBody) {
+    public function sendBody(int $iCode, string $sBody): void
+    {
         $this->sendHttpStatusheader($iCode);
         die('<!doctype html><html><head>'
-                . '<title>Redirect</title>'
-                . '<style>'
-                . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}'
-                . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}'
-                . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}'
-                . 'h2 small{font-size: 50%;}'
-                . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }'
-                . '</style>'
-                . '</head>'
-                . '<body>'
-                . $sBody
-                . '<h2>' . $this->sAbout . '</h2>'
-                . '<footer>&copy; ' . date('Y') . ' '.$this->sHostname.'</footer>'
-                . '</body></html>'
+            . '<title>Redirect</title>'
+            . '<style>'
+            . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}'
+            . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}'
+            . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}'
+            . 'h2 small{font-size: 50%;}'
+            . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }'
+            . '</style>'
+            . '</head>'
+            . '<body>'
+            . $sBody
+            . '<h2>' . $this->sAbout . '</h2>'
+            . '<footer>&copy; ' . date('Y') . ' ' . $this->sHostname . '</footer>'
+            . '</body></html>'
         );
     }
 
-
 }