diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile
index 75b09744aa9c284855cb84e6cbe0340e3027fdb3..db0a24c565fd84312209140c8d9a0de79a50e1c2 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
 #
-FROM php:8.3-apache
+FROM php:8.4-apache
 
 # install packages
 RUN apt-get update && apt-get install -y git unzip zip
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 4fa133ff15feb1815a239591c1f718fa7cc5d21c..951f3e7cf594772d1e7999d547fedb86a016ab89 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -19,7 +19,7 @@ services:
     build:
       context: .
       dockerfile: ./containers/web-server/Dockerfile
-    image: "php:8.3-apache"
+    image: "php:8.4-apache"
     container_name: 'my_new_app-server'
     ports:
       - '${APP_PORT}:80'
diff --git a/docker/init.sh b/docker/init.sh
index d75223516bd09e854db9378231f85b42b1ba4e70..04fc725623d657387c79c9ad4644cffe7c5bbc95 100755
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -24,20 +24,28 @@
 # 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)
+# 2024-10-16  v1.20 <axel.hahn@unibe.ch>      add db import and export
+# 2024-10-25  v1.21 <axel.hahn@unibe.ch>      create missing subdir dbdumps
+# 2024-10-30  v1.22 <axel.hahn@unibe.ch>      added: Open Mysql client in container
+# 2024-10-30  v1.23 <axel.hahn@unibe.ch>      added: show menu hints why some menu items are visible
+# 2024-11-20  v1.24 <axel.hahn@unibe.ch>      fix menu with started database less app; apply template permissions on target file; add $WEBURL; remove $frontendurl
+# 2024-11-20  v1.25 <axel.hahn@unibe.ch>      fix menu startup containers
+# 2024-11-21  v1.26 <axel.hahn@unibe.ch>      Reset colors in _checkConfig 
 # ======================================================================
 
 cd "$( dirname "$0" )" || exit 1
 
+_version="1.26"
+
 # init used vars
 gittarget=
-frontendurl=
+WEBURL=
 
 _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"
@@ -55,14 +63,19 @@ fgReset="\e[0m"
 # running containers
 DC_WEB_UP=0
 DC_DB_UP=0
+DC_ALL_UP=0
 
 # repo of docker-php-starterkit is here?
 DC_REPO=1
 
 DC_CONFIG_CHANGED=0
 
+# absolute urls for web app
 DC_WEB_URL=""
 
+DC_DUMP_DIR=dbdumps
+DC_SHOW_MENUHINTS=0
+
 isDockerRootless=0
 ps -ef | grep  dockerd-rootless | grep -q $USER && isDockerRootless=1
 
@@ -70,6 +83,19 @@ ps -ef | grep  dockerd-rootless | grep -q $USER && isDockerRootless=1
 # FUNCTIONS
 # ----------------------------------------------------------------------
 
+# check config for changes in newer versions
+function _checkConfig(){
+
+    # --- v1.24
+    if [ -z "$WEBURL" ]; then
+        echo -e "${fgBrown}INFO: add 'WEBURL=\"/\"' in your ${_self}.cfg. It is a new var since v1.24${fgReset}"
+        WEBURL="/"
+    fi
+    if [ -n "$frontendurl" ]; then
+        echo -e "${fgBrown}INFO: Remove frontendurl=$frontendurl in your ${_self}.cfg. It is obsolete since v1.24${fgReset}"
+    fi
+
+}
 # ----------------------------------------------------------------------
 # STATUS FUNCTIONS
 
@@ -97,13 +123,38 @@ function _getStatus_docker(){
 
     DC_WEB_UP=0
     DC_DB_UP=0
+    DC_ALL_UP=0
+
     grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1
     grep -q "${APP_NAME}-db"     <<< "$_out"  && DC_DB_UP=1
+
+    if [ "$DB_ADD" != "false" ] && [ ! -d "${DC_DUMP_DIR}" ]; then
+        echo "INFO: creating subdir ${DC_DUMP_DIR} to import/ export databases ..."
+        mkdir "${DC_DUMP_DIR}" || exit 1
+        return
+    fi
+
+    if [ "${DC_WEB_UP}" = "1" ] && [ "${DC_DB_UP}" = "1" ]; then
+        DC_ALL_UP=1
+    fi
+
+    if [ "$DB_ADD" = "false" ] && [ "${DC_WEB_UP}" = "1" ]; then
+        DC_ALL_UP=1
+    fi
+
 }
 
+# Get web url of the application
+# It is for support of Nginx Docker Proxy
+# https://github.com/axelhahn/nginx-docker-proxy
+# It returns http://localhost:<port> or a https://<appname> plus $WEBURL
 function _getWebUrl(){
-    DC_WEB_URL="$frontendurl"
-    grep -q "${APP_NAME}-server" /etc/hosts && DC_WEB_URL="https://${APP_NAME}-server/"
+    if grep -q "^[0-9\.]* ${APP_NAME}-server" /etc/hosts; then
+        DC_WEB_URL="https://${APP_NAME}-server$WEBURL"
+    else
+        DC_WEB_URL=http://localhost:${APP_PORT}$WEBURL
+    fi
+    set +vx
 }
 
 # ----------------------------------------------------------------------
@@ -123,7 +174,16 @@ function h3(){
 
 # helper for menu: print an inverted key
 function  _key(){
-    echo -en "\e[4;7m ${1} \e[0m"
+    echo -en "$fgInvert ${1} $fgReset"
+}
+
+# helper for menu: show hint text
+# param  int      FLag _bAll (i true the txt will be hidden)
+# param  string   message to show
+function menuhint(){
+    local _bAll="$1"
+    shift 1
+    test $DC_SHOW_MENUHINTS -ne 0 && test "$_bAll" -eq "0" && ( echo -e "$fgBlue  $*$fgReset" )
 }
 
 # show menu in interactive mode and list keys in help with param -h
@@ -137,38 +197,61 @@ function showMenu(){
 
     echo
     if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Git data of starterkit were found"
         echo "${_spacer}$( _key g ) - remove git data of starterkit"
         echo
     fi
 
     if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Because rootless docker was found"
         echo "${_spacer}$( _key i ) - init application: set permissions"
+        echo
     fi
 
     if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Config was changed"
         echo "${_spacer}$( _key t ) - generate files from templates"
+        echo
     fi
     if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Config is unchanged"
         echo "${_spacer}$( _key T ) - remove generated files"
+        echo
     fi
-    echo
-    if [ $DC_WEB_UP -eq 0 ] || [ $_bAll -eq 1 ]; then
+    if [ $DC_ALL_UP -eq 0 ] || [ $_bAll -eq 1 \
+    ]; then
         if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+            menuhint $_bAll "A container is down and config is unchanged"
             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"
+            echo
         fi
     fi
-    if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+    if [ $DC_WEB_UP -eq 1 ] || [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "A container is up"
         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
+    fi
+    if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Web container is up"
         echo "${_spacer}$( _key p ) - console check with php linter"
+        echo
     fi
-    echo
+    if [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo
+        menuhint $_bAll "Database container is up"
+        echo "${_spacer}$( _key d ) - Dump container database"
+        echo "${_spacer}$( _key D ) - Import Dump into container database"
+        echo "${_spacer}$( _key M ) - Open Mysql client in database container"
+        echo
+    fi
+    menuhint $_bAll "Always available"
     echo "${_spacer}$( _key q ) - quit"
 
 }
@@ -313,7 +396,7 @@ function _fix_no-db(){
         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 -n "$sed_no_backup" "1,${iStart}p" "${_file}"
         fi
     fi
 }
@@ -375,14 +458,15 @@ function _generateFiles(){
 
             # write file from line 2 to a tmp file
             sed -n '2,$p' "$mytpl" >"$_tmpfile"
+            chmod "$( stat -c %a "$mytpl" )" "$_tmpfile"
 
             # add generator
             # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile
             local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' )
-            sed -i "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile
+            sed -i "$sed_no_backup" "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile
 
             # apply all replacements to the tmp file
-            eval sed -i "$params" "$_tmpfile" || exit
+            eval sed "$sed_no_backup" "$params" "$_tmpfile" || exit
 
             _fix_no-db $_tmpfile
 
@@ -395,7 +479,7 @@ function _generateFiles(){
                     echo -n "$mytpl - changes detected - writing [$target] ... "
                     mkdir -p "$( dirname  ../"$target" )" || exit 2
                     mv "$_tmpfile" "../$target" || exit 2
-                    echo OK
+                    echo -e "${fgGreen}OK${fgReset}"
                     echo
                 fi
             else
@@ -424,7 +508,7 @@ function _removeGeneratedFiles(){
             echo -n "REMOVING "
             ls -l "../$target" || exit 2
             rm -f "../$target" || exit 2
-            echo OK
+            echo -e "${fgGreen}OK${fgReset}"
         else
             echo "SKIP: $target"
         fi
@@ -498,10 +582,94 @@ function _wait(){
     echo
 }
 
+# DB TOOL - dump db from container
+function _dbDump(){
+    local _iKeepDumps;
+    typeset -i _iKeepDumps=5
+    local _iStart;
+    typeset -i _iStart=$_iKeepDumps+1;
+
+    if [ $DC_DB_UP -eq 0 ]; then
+        echo "Database container is not running. Aborting."
+        return
+    fi
+    outfile=${DC_DUMP_DIR}/${MYSQL_DB}_$( date +%Y%m%d_%H%M%S ).sql
+    echo -n "dumping ${MYSQL_DB} ... "
+    if docker exec -i "${APP_NAME}-db" mysqldump -uroot -p${MYSQL_ROOT_PASS} ${MYSQL_DB} > "$outfile"; then
+        echo -n "OK ... Gzip ... "
+        if gzip "${outfile}"; then
+            echo "OK"
+            ls -l "$outfile.gz"
+
+            # CLEANUP
+            echo
+            echo "--- Cleanup: keep $_iKeepDumps files."
+            ls -1t ${DC_DUMP_DIR}/* | sed -n "$_iStart,\$p" | while read -r delfile
+            do 
+                echo "CLEANUP: Deleting $delfile ... "
+                rm -f "$delfile"
+            done
+            echo
+            echo -n "Size of dump directory: "
+            du -hs ${DC_DUMP_DIR} | awk '{ print $1 }'
+
+        else
+            echo "ERROR"
+            rm -f "$outfile"
+        fi
+    else
+        echo "ERROR"
+        rm -f "$outfile"
+    fi
+}
+
+# DB TOOL - import local database dump into container
+function _dbImport(){
+    echo "--- Available dumps:"
+    ls -ltr ${DC_DUMP_DIR}/*.gz | sed "s#^#    #g"
+    if [ $DC_DB_UP -eq 0 ]; then
+        echo "Database container is not running. Aborting."
+        return
+    fi
+    echo -n "Dump file to import into ${MYSQL_DB} > "
+    read -r dumpfile
+    if [ -z "$dumpfile" ]; then
+        echo "Abort - no value was given."
+        return
+    fi
+    if [ ! -f "$dumpfile" ]; then
+        echo "Abort - wrong filename."
+        return
+    fi
+
+    echo -n "Importing $dumpfile ... "
+
+    # Mac OS compatibility
+    # if zcat "$dumpfile" | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+    if cat "$dumpfile" | zcat | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+    then
+        echo "OK"
+    else
+        echo "ERROR"
+    fi
+}
+
 # ----------------------------------------------------------------------
 # MAIN
 # ----------------------------------------------------------------------
 
+_checkConfig
+
+# Mac OS compatibility
+case "$OSTYPE" in
+  darwin*|bsd*)
+    sed_no_backup=" -i '' "
+    ;; 
+  *)
+    sed_no_backup="-i"
+    ;;
+esac
+
 action=$1; shift 1
 
 while true; do
@@ -612,6 +780,18 @@ while true; do
                 echo "Start your docker container first."
             fi
             ;;
+        d) 
+            h2 "DB tools :: dump"
+            _dbDump
+            ;;
+        D) 
+            h2 "DB tools :: import"
+            _dbImport
+            ;;
+        M)
+            h2 "DB tools :: mysql client"
+            docker exec -it "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+            ;;
         o) 
             h2 "Open app ..."
             xdg-open "$DC_WEB_URL"
diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg
index e30c69e0a9664ebff0532f299a6af60826959057..c66e959ddd215ece6fa988b277b4dbc19a325a51 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.3
+APP_PHP_VERSION=8.4
 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug"
 # APP_PHP_MODULES="curl mbstring xml zip xdebug"
 APP_PHP_MODULES="xdebug"
@@ -61,9 +61,8 @@ DOCKER_USER_UID=33
 
 # document root inside web-server container 
 WEBROOT=/var/www/${APP_NAME}/public_html
+WEBURL=/admin/
 
 CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE"
 
-frontendurl=http://localhost:${APP_PORT}/
-
 # ----------------------------------------------------------------------
diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md"
index 372e2f38a6168e613b1b932853b13353859d53a6..7fbf9f88e1518065a2ee47400586a90a1df6abfd 100644
--- "a/docs/10_\360\237\223\221_Description.md"
+++ "b/docs/10_\360\237\223\221_Description.md"
@@ -1,6 +1,19 @@
+## Why?
+
+At our institute we have about 50 domains for redirections only.
+If all rewrite rules are inside wbserver definitions then no non-sysadmin would be able to get an overview over all redirect domains and their rules to point anywhere with what redirect code.
+
+All domains with redirects only point to the same web that executes the redirects.
+
+The redirects are handled in JSON files. So it can be put into a repository and can reproduce the history of changes.
+
+Next to the redirec functionality for the public web there is a web ui for showing all domains, redirects in a sortable, searchable table. Domains and redirects can be tested.
+
+![Admin ui](images/admin_ui.png)
+
 ## Requirements
 
-* PHP 8 (up to PHP 8.3)
+* PHP 8 (up to PHP 8.4)
 * Webserver (docs describe usage for Apache httpd)
 
 ## Features
diff --git "a/docs/20_\342\226\266\357\270\217_Installation.md" "b/docs/20_\342\226\266\357\270\217_Installation.md"
index 37b0e1ab2e027512d75931467406b3b74d0284ca..000ccd9d06cfd92a0263346a9f0a2cde87b4f67a 100644
--- "a/docs/20_\342\226\266\357\270\217_Installation.md"
+++ "b/docs/20_\342\226\266\357\270\217_Installation.md"
@@ -2,8 +2,16 @@
 
 ### Get the files
 
-The repository contains a subfolder *public_html*. Run `git clone` or extract the downloaded archive 1 level above webroot. The document root of the web must point
-to the public_html directory. The config folder is outside the webroot.
+The repository contains a subfolder *public_html*. Run `git clone` or extract the downloaded archive 1 level above webroot. 
+
+In this example the software will be installed into a webroot `/var/www/links/public_html`:
+
+```shell
+mkdir /var/www/
+git clone https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler.git links
+```
+
+The document root of the web must point to the public_html directory. The config folder is outside the webroot.
 
 ```text
 .
@@ -23,21 +31,89 @@ to the public_html directory. The config folder is outside the webroot.
 └── readme.md
 ```
 
-### Redirect all requests to index.php
+### Webserver
+
+This is the basic idea how it works:
+
+* Create on or multiple vhosts with document root /var/www/links/public_html
+* optional: one extra domain eg. links.example.com points to /var/www/links/public_html too - and is restricted with ip restriction, basic auth, whatever.
+
+#### Redirect all requests to index.php
 
 Redirect all requests to the index.php. Activate the .htaccess or (better)
 add the config to the vhost config.
 
 ```text
-RewriteEngine On
-RewriteBase /
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteCond %{REQUEST_FILENAME} !-d
-RewriteRule ^.*$ /index.php [L]
+<VirtualHost *:80>
+  ServerName redirects.example.com
+  ServerAlias www.redirect-domain-1.com ... www.redirect-domain-N.com
+
+  DocumentRoot "/var/www/links/public_html"
+  ServerSignature Off
+ 
+  ErrorLog  "/var/log/apache2/links_error.log"
+  CustomLog "/var/log/apache2/links_access.log" combined 
+   
+  # --- Allow access on webroot
+  <Directory "/var/www/links/public_html">
+    Options -Indexes -FollowSymLinks -MultiViews
+    AllowOverride None
+    Require all granted
+  </Directory>
+
+  ## Rewrite rules
+  RewriteEngine On
+
+  RewriteRule ^(.*)$ index.php [QSA,L]
+
+</VirtualHost>
 ```
 
 In the DNS point all hostnames with redirects only to this server (i.e. with 
 a CNAME).
 
-If you don't have a single vhost in the webserver then additionally add the
-domains to "catch" as ServerAlias.
+#### Web ui
+
+The web ui is a viewer only - no configuration can be changed.
+It shows all domains, redirects and is helpful to keep an overview.
+
+This web points to the same document root - should be protected. In the example below is an ip restriction with an additional basic auth (snippet only).
+
+```text
+<VirtualHost *:443>
+  ServerName links.example.com
+  DocumentRoot "/var/www/links/public_html"
+  ServerSignature Off
+
+  ErrorLog  "/var/log/apache2/links_error.log"
+  CustomLog "/var/log/apache2/links_access.log" combined 
+   
+  # --- Allow access on webroot
+  <Directory "/var/www/links/public_html">
+    Options -Indexes -FollowSymLinks -MultiViews
+    AllowOverride None
+    Require all granted
+
+    <RequireAll>
+        # ip restriction: networks with access
+        <RequireAny>
+            Require ip 192.168.100.0/24
+            Require ip 192.168.200.0/24
+        </RequireAny>
+
+        # and additional basic auth
+        Require valid-user
+        AuthType Basic
+        # ... basic auth config here
+    </RequireAll>
+
+  </Directory>
+
+    SSLEngine on
+    SSLCertificateFile      "/etc/ssl/certs/links.example.com.fullchain.cer"
+    SSLCertificateKeyFile   "/etc/ssl/certs/links.example.com.key.pem"
+    SSLCertificateChainFile "/etc/ssl/certs/links.example.com.fullchain.cer"
+    SSLCACertificatePath    "/etc/ssl/certs"
+
+</VirtualHost>
+```
\ No newline at end of file
diff --git "a/docs/30_\342\232\231\357\270\217_Configuration.md" "b/docs/30_\342\232\231\357\270\217_Configuration.md"
index 9045a308fd74ee3c2befba62caf6be3faa0789a0..cb6580c058bb023e3b507fdd9a849518c335d381 100644
--- "a/docs/30_\342\232\231\357\270\217_Configuration.md"
+++ "b/docs/30_\342\232\231\357\270\217_Configuration.md"
@@ -21,6 +21,8 @@ Let's start with an example for a `redirects_<FQDN>.json`:
 
 ```json
 {
+    "comment": "Example redirect web",
+    "httponly": true,
     "direct":{
         "/heise":   {"code": 307, "target": "https://www.heise.de" }
     },
@@ -33,10 +35,14 @@ Let's start with an example for a `redirects_<FQDN>.json`:
 }
 ```
 
-There are 2 required sections to define redirects:
+The keys are
 
-* direct
-* regex
+| Key      | Type   | comment |
+| ---      |---     | ---
+| comment  | string | optional: short description for this domain |
+| httponly | bool   | optional: flag if the redirects work on http only. Default: false (redirect definitions are the same for http and https) |
+| direct   | array  | required: list of redirects with strings    |
+| regex    | array  | required: list of redirects with regex      |
 
 The section "direct" will be scanned first and has priority.
 The json will be read into a hash ... if you define a direct rule twice then
diff --git "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
index c69d22a1be4209d3f63264bd8fdd9896b8b4b67c..5ee6e8be789841f82c8adca79087c5852e9715fb 100644
--- "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
+++ "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
@@ -25,7 +25,7 @@ In the table you see the columns
 
 * **Host** - the hostname/ FQDN that has a redirect rule
   * πŸ”· Domain with config entries
-  * ◻️ Alias pointng to the config of another domain
+  * β–ͺ️ Alias pointng to the config of another domain
 * **Ip address** - the found ip of the fqdn. 
   * 🟒 green = domain is on the same ip like the redirect tool.
   * 🟠 yellow = domain is on another domain (= the config from here does not work because redirect is handled on another system)
@@ -36,12 +36,22 @@ In the table you see the columns
 * **Code** - http response code for redirection
 * **Target** - target url
 
-## Http header
+### Test connection
+
+You can click a link to a hostname, a linked redirect or target url. In an overlay window you get the status for the http test.
+
+### Http header
 
 If click on the link of the host you get a live view of a response of a http HEAD request to the webroot of this domain.
 
 The redirects of the type "direct" have a linked "From" field value. If you click on it you get a live view of the http response header of `https://[Server]/[From]`.
 
+![Admin ui: request successful](images/admin_ui_box_http_ok.png)
+
+If the connect failed there is no http response header to show. To get an idea what is wrong you get the curl error message. The you can estimate the problem - is it a missing DNS entry, a network problem, timeout, whatever.
+
+![Admin ui: connection failed](images/admin_ui_box_connect_failed.png)
+
 ## Config
 
 If you click on the linked text "config" you get the content of the config file for this domain with all its redirects.
diff --git a/docs/images/admin_ui.png b/docs/images/admin_ui.png
index a7f9671a7a69da673f55571a7e127a94848f8b09..e3c36b2946632c22b21a9ee80a81e40b0022dde8 100644
Binary files a/docs/images/admin_ui.png and b/docs/images/admin_ui.png differ
diff --git a/docs/images/admin_ui_box_connect_failed.png b/docs/images/admin_ui_box_connect_failed.png
new file mode 100644
index 0000000000000000000000000000000000000000..c30be6b0137b6197e39a0364bb465da350567a41
Binary files /dev/null and b/docs/images/admin_ui_box_connect_failed.png differ
diff --git a/docs/images/admin_ui_box_http_ok.png b/docs/images/admin_ui_box_http_ok.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea2e0c841d756ef3707abfd0c5a93a4494652577
Binary files /dev/null and b/docs/images/admin_ui_box_http_ok.png differ
diff --git a/docs/style.css b/docs/style.css
index 45383c3288f17a74d88fc2ffca48307f794d4592..90a0bc938f147608a1d4fd11952e4825f076862c 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -1,6 +1,6 @@
 /*
     override css elements of daux.io blue theme
-    version 2023-11-10
+    version 2024-10-31
 */
 :root {
     /* Axels Overrides */
@@ -8,22 +8,24 @@
     --link-color: #228;
     --brand-color: var(--color-text);
     --brand-background: var(--body-background);
-    --code-tag-border-color: #d8d8d8;
+    --code-tag-background-color: #f0f3f3;
+    --code-tag-border-color: #dee;
+    --code-tag-box-shadow: none;
     --hr-color: none;
+    --pager-background-color: #f8fafa;
+    --pager-border-color: none;
     --search-field-background: none;
     --search-field-border-color: none;
     --sidebar-background: var(--body-background);
     --sidebar-border-color: none;
-    --sidebar-link-active-background: #e8f4f6;
-    --sidebar-link-active-background: #eee;
+    --sidebar-link-active-background: #f0f4f6;
+    --toc--inner-border-color: none;
     /* Axels custom values */
-    --axel_bg-toc: var(--body-background);
+    --axel_bg-toc: #f8fafa;
     --axel_bg-toc-head: #f8f8f8;
     --axel_brand-background: none;
     --axel_brand-pre-background: rgb(255, 0, 51);
-    ;
     --axel_brand-pre-background-hover: rgb(255, 0, 51);
-    ;
     --axel_h1_header: none;
     --axel_h1: #111;
     --axel_h1-bg: none;
@@ -34,12 +36,13 @@
     --axel_h2-hero-bottom: 2px solid #912;
     --axel_h3: #333;
     --axel_h3-bottom: 0px solid #ddd;
-    --axel_h4: #444;
-    --axel_hero_bg: #f8f8f8;
+    --axel_h4: #666;
+    --axel_h5: #888;
+    --axel_hero_bg: #faf8f6;
     --axel_img-border: 2px dashed #ccc;
     --axel_nav-bg: #fcfcfc;
     --axel_nav-buttomborder: #ddd;
-    --axel_pre-background: #f8f8f8;
+    --axel_pre-background: #faf8f6;
     --axel-th-background: #e0e4e8;
     --axel-article-nav-border-top: 0px dotted #ddd;
 }
@@ -58,6 +61,7 @@
     --sidebar-background: var(--body-background);
     --sidebar-border-color: none;
     --sidebar-link-active-background: #333;
+    --sidebar-link-color: var(--link-color);
     /* Axels custom values */
     --axel_bg-toc: var(--body-background);
     --axel_bg-toc-head: #333;
@@ -76,6 +80,8 @@
     --axel_h2-hero-bottom: 2px solid #712;
     --axel_h3: #589;
     --axel_h3-bottom: 0px solid #333;
+    --axel_h4: #478;
+    --axel_h5: #278;
     --axel_hero_bg: #242424;
     --axel_img-border: 2px dashed #555;
     --axel_nav-bg: #242424;
@@ -122,7 +128,25 @@ a.Brand {
 
 /* ---------- page content ---------- */
 .s-content {
-    padding-top: 1em;
+    padding-top: 6em;
+}
+
+/**
+h1::before{color: #aaa;content: 'h1: ';}
+h2::before{color: #aaa;content: 'h2: ';}
+h3::before{color: #aaa;content: 'h3: ';}
+h4::before{color: #aaa;content: 'h4: ';}
+h5::before{color: #aaa;content: 'h5: ';}
+h6::before{color: #aaa;content: 'h6: ';}
+*/
+h2::before{color: #888;content: ': : ';}
+h3::before{color: #ccc;content: '> ';}
+h4::before{color: #ccc;content: '_ ';}
+
+.s-content h1::before{
+    color: #f00;
+    content: 'FEHLER: Keine Überschrift 1 in einer Markdown-Datei für Daux verwenden! Mit H2 beginnen!';
+    content: '!! h1 !! ';
 }
 
 .s-content h1 {
@@ -144,8 +168,14 @@ a.Brand {
     border-bottom: var(--axel_h2-bottom);
 }
 
-h1:first-of-type {
+.Page__header > h1:first-of-type {
     margin-top: 0em;
+    margin-left: -1em;
+    padding-left: 1em;
+    position: fixed;
+    min-width: 100%;
+    background: var(--body-background);
+    box-shadow: 0 2em 1em var(--body-background);
 }
 
 h2:first-of-type {
@@ -170,6 +200,13 @@ img{
 
 .s-content > h4 {
     color: var(--axel_h4);
+    font-size: 140%;
+    font-weight: bold;
+    margin: 2em 0;
+}
+
+.s-content > h5 {
+    color: var(--axel_h5);
     font-size: 135%;
     font-weight: bold;
     margin: 2em 0;
@@ -189,6 +226,8 @@ ul.TableOfContents a{
 }
 .s-content pre {
     background: var(--axel_pre-background);
+    border-radius: 0.5em;
+    padding: 1rem;
 }
 
 /* FIX smaller fnt size in tables */
@@ -240,13 +279,6 @@ div.hero h2 {
 }
 
 /* ---------- TOC ---------- */
-@media(min-width:1700px) {
-    .TableOfContentsContainer {
-        position: fixed;
-        right: 2em;
-        top: 1em;
-    }
-}
 
 .TableOfContentsContainer {
     background-color: var(--axel_bg-toc);
@@ -256,14 +288,18 @@ div.hero h2 {
 .s-content .TableOfContentsContainer h4 {
     background-color: var(--axel_bg-toc-head);
     border-top-left-radius: 1em;
+    border-bottom: 2px solid var(--axel_bg-toc-bottom-border);
     font-size: 1.1em;
     margin: 0;
-    padding: 0;
+    padding: 0.3em;
+    display: none;
 }
 
 .TableOfContentsContainer__content {
-    border-width: 1px;
+    border-width: 0px;
     font-size: 0.5em;
+    height: inherit; 
+    overflow: auto;
 }
 
 ul.TableOfContents ul {
@@ -271,17 +307,27 @@ ul.TableOfContents ul {
     padding-left: 1em;
 }
 
+.TableOfContents a:hover{
+    text-decoration: underline;
+}
+
+@media(min-width:1700px) {
+    .TableOfContentsContainer {
+        background: none;
+        position: fixed;
+        right: 2em;
+        top: 4em;
+        height: 90%;
+    }
+}
+
 /* ----- Icons on links --- */
 
 .EditOn a::before{
     content: '✏️ ';
 }
 
-.Links a[href^="https://github.com/"]::before {
-    content: '🌐 ';
-}
-
-.Links a[href^="https://git-repo.iml.unibe.ch/"]::before {
+.Links a::before {
     content: '🌐 ';
 }
 
diff --git a/public_html/admin/functions.js b/public_html/admin/functions.js
index 7650f5d31ce6382ddfb9581ec680a12c795a0871..b5feeee70541c9c91792d2b95ca4ef8fea5bcf69 100644
--- a/public_html/admin/functions.js
+++ b/public_html/admin/functions.js
@@ -44,6 +44,12 @@ async function showInOverlay(oLink){
 
     if (response.ok) {
       show(body);
+
+      // colorize clicked link
+      var myspan=document.getElementsByClassName("status")[0];
+      var myclass=myspan.className.replace("status ","");
+      oLink.className=myclass;
+      oLink.title=myspan.innerText;
     } else {
       show("HTTP-Error: " + response.status);
     }
diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index f09c4897bb47ad6434516e32bac79df9227e3d15..185914d25e921b7a67cf44faf8dacd7bad7e3291 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -14,47 +14,46 @@
  * 2022-02-03  v0.1  <axel.hahn@iml.unibe.ch>  initial version 
  * 2022-05-31  v0.2  <axel.hahn@iml.unibe.ch>  optical changes; use debugredirect=1 if url is a local domain
  * 2023-08-28  v1.0  <axel.hahn@unibe.ch>      Welcome message if there is no config yet
+ * 2025-01-13  v1.1  <axel.hahn@unibe.ch>      test links http and https
  * ----------------------------------------------------------------------
  */
 
 require_once '../classes/redirect.admin.class.php';
-$oR=new redirectadmin();
-$sHtml='';
-$sErrors='';
+$oR = new redirectadmin();
+$sHtml = '';
+$sErrors = '';
 
-$aIco=[
-    'h1'=>'⏩',
+$aIco = [
+    'h2_err' => '⚠️',
+    'h2_config' => 'πŸ› οΈ',
+    'h2_file' => 'πŸ“„',
 
-    'h2_head'=>'βœ”οΈ',
-    'h2_err'=>'⚠️',
-    'h2_config'=>'πŸ› οΈ',
-    'h2_file'=>'πŸ“„',
+    'ip_ok' => '🟒',
+    'ip_warn' => '🟠',
+    'ip_err' => '❗',
 
-    'ip_ok'=>'🟒',
-    'ip_warn'=>'🟠',
-    'ip_err'=>'❗',
+    'type_config' => 'πŸ”·',
+    'type_alias' => 'β–ͺ️',
 
-    'type_config'=>'πŸ”·',
-    'type_alias'=>'◻️',
-
-    'url'=>'🌐',
-    'welcome'=>'πŸͺ„',
+    'url' => '🌐',
+    'welcome' => 'πŸͺ„',
 ];
 
 // ----------------------------------------------------------------------
 // FUNCTIONS
 // ----------------------------------------------------------------------
 
-function getId($sDomain){
-    return 'id_'.md5($sDomain);
+function getId($sDomain)
+{
+    return 'id_' . md5($sDomain);
 }
 
 // ----------------------------------------------------------------------
 // MAIN
 // ----------------------------------------------------------------------
 
-if (!$oR->isEnabled()){
-    $sHtml.='<div class="content">
+if (!$oR->isEnabled()) {
+    $sHtml .= '<div class="content">
         <h3>Nothing to see here.</h3>
         <div class="error">
             The Admin interface is disabled.
@@ -66,148 +65,154 @@ if (!$oR->isEnabled()){
     // ---------- return content for ajax requests
 
     // ---------- TEST URL
-    $sUrl=(isset($_GET['url']) && $_GET['url']) ? $_GET['url'] : '';
+    $sUrl = (isset($_GET['url']) && $_GET['url']) ? $_GET['url'] : '';
 
-    if ($sUrl){
-        $sResult=$oR->httpGet($sUrl,1);
-        echo '<h2>Response of a http HEAD request to '.$aIco['url'].' <a href="'.$sUrl.'">'.$sUrl.'</a></h2>'
-            . $oR->renderHttpResponseHeader($sResult)
-            ;
+    if ($sUrl) {
+        $aResult = $oR->httpGet($sUrl, 1);
+        echo '<h2>Response of a http HEAD request to ' . $aIco['url'] . ' <a href="' . $sUrl . '">' . $sUrl . '</a></h2>'
+            . $oR->renderHttpResponseHeader($aResult)
+        ;
         return true;
     }
     # ---------- return content for ajax requests
-    $sCfgfile=(isset($_GET['cfgfile']) && $_GET['cfgfile']) ? $_GET['cfgfile'] : '';
-    if($sCfgfile){
-        $sFilename=__DIR__.'/../../config/'.$sCfgfile;
-        echo '<h2>'.$aIco['h2_file'].' File: '.htmlentities($sCfgfile).'</h2>'
+    $sCfgfile = (isset($_GET['cfgfile']) && $_GET['cfgfile']) ? $_GET['cfgfile'] : '';
+    if ($sCfgfile) {
+        $sFilename = __DIR__ . '/../../config/' . $sCfgfile;
+        echo '<h2>' . $aIco['h2_file'] . ' File: ' . htmlentities($sCfgfile) . '</h2>'
             . (file_exists($sFilename)
-                ? '<pre>'. file_get_contents($sFilename).'</pre>'
+                ? '<pre>' . file_get_contents($sFilename) . '</pre>'
                 : '<div class="error">ERROR: config file was not found.</div>'
             )
         ;
         exit(0);
     }
 
-    $sMyIp=gethostbyname($_SERVER['SERVER_NAME']);
-    if(!$sMyIp){
-        $sErrors.='<li>Ip address of current host ['.$_SERVER['SERVER_NAME'].'] was not found.</li>';
+    $sMyIp = gethostbyname($_SERVER['SERVER_NAME']);
+    if (!$sMyIp) {
+        $sErrors .= '<li>Ip address of current host [' . $_SERVER['SERVER_NAME'] . '] was not found.</li>';
     }
 
     // ---------- GET CONFIG DATA
 
-    $aHosts=$oR->getHosts();
+    $aHosts = $oR->getHosts();
 
     // ---------- SHOW ERRORS
 
-    if(count($aHosts['_errors'])) {
-        $sErrors.= '<li>' . implode('</li><li>', $aHosts['_errors']).'</li>';
+    if (count($aHosts['_errors'])) {
+        $sErrors .= '<li>' . implode('</li><li>', $aHosts['_errors']) . '</li>';
     }
     unset($aHosts['_errors']);
 
     // ---------- LOOP OVER ALL ENTRIES
 
-    $sTable='';
-    foreach($aHosts as $sHost => $aCfg){
-        $sTdFirst='<tr class="cfgtype-'.$aCfg['type'].'">'
-            .'<td>'
-                .'<span style="display: none">'.$sHost.'</span>'
-                .$aIco['type_'.$aCfg['type']]
-                .' <a href="?url=http://'.$sHost.'/'
-                    .($aCfg['ip']===$sMyIp ? '?debugredirect=1' : '' )
-                    .'" title="click to test http://'.$sHost.'/" onclick="showInOverlay(this); return false;">'.$sHost.'</a></td>'
-            .'<td>'
-                .($aCfg['ip'] 
-                    ? ($aCfg['ip']===$sMyIp
-                        ? '<span title="">'.$aIco['ip_ok'].' '.$aCfg['ip']. '</span>'
-                        : '<span title="Warning: this is not the ip address of the current host ('.$sMyIp.')">'.$aIco['ip_warn'].' '.$aCfg['ip']. '</span>'
-                    )
-                    : '<span class="error">'.$aIco['ip_err'].' ERROR: unknown host</span>')
-            .'</td>'
-            .'<td>'
-            .($aCfg['type']=="config"
-                ? '<a href="?cfgfile=redirects_'.$sHost.'.json" onclick="showInOverlay(this); return false;" title="show config for host '.$sHost.'">'.$aCfg['type'].'</a> '
-                : '<a href="?cfgfile=redirects_'.$aCfg['target'].'.json" onclick="showInOverlay(this); return false;" title="show config for alias '.$sHost.' pointing to host '.$aCfg['target'].'">'.$aCfg['type'].'</a> '
+    $sTable = '';
+    foreach ($aHosts as $sHost => $aCfg) {
+        $bHttpOnly = isset($aCfg['redirects']['httponly']) && $aCfg['redirects']['httponly'];
+
+        $sUrlpart = '://' . $sHost . '/'
+            . ($aCfg['ip'] === $sMyIp ? '?debugredirect=1' : '')
+        ;
+        $sTdFirst = '<tr class="cfgtype-' . $aCfg['type'] . '">'
+            . '<td>'
+            . '<span style="display: none">' . $sHost . '</span>'
+            . '<span style="float: right;">'
+            . ' <a href="?url=http' . $sUrlpart . '"  title="click to test http://' . $sHost . '/"  onclick="showInOverlay(this); return false;">http</a> '
+            . ($bHttpOnly ? '' : '<a href="?url=https' . $sUrlpart . '" title="click to test https://' . $sHost . '/" onclick="showInOverlay(this); return false;">https</a>')
+            . '</span>'
+            . $aIco['type_' . $aCfg['type']] . " $sHost"
+            . '</td>'
+            . '<td>'
+            . ($aCfg['ip']
+                ? ($aCfg['ip'] === $sMyIp
+                    ? '<span title="">' . $aIco['ip_ok'] . ' ' . $aCfg['ip'] . '</span>'
+                    : '<span title="Warning: this is not the ip address of the current host (' . $sMyIp . ')">' . $aIco['ip_warn'] . ' ' . $aCfg['ip'] . '</span>'
+                )
+                : '<span class="error">' . $aIco['ip_err'] . ' ERROR: unknown host</span>')
+            . '</td>'
+            . '<td>'
+            . ($aCfg['type'] == "config"
+                ? '<a href="?cfgfile=redirects_' . $sHost . '.json" onclick="showInOverlay(this); return false;" title="show config for host ' . $sHost . '">' . $aCfg['type'] . '</a> '
+                : '<a href="?cfgfile=redirects_' . $aCfg['target'] . '.json" onclick="showInOverlay(this); return false;" title="show config for alias ' . $sHost . ' pointing to host ' . $aCfg['target'] . '">' . $aCfg['type'] . '</a> '
             )
             . '</td>'
-            ;
-        /*
-        if(!$aCfg['ip']){
-            $sErrors.='<li>Host was not found in DNS: '.$sHost.'</li>';
-        }
-        */
-        if (isset($aCfg['redirects'])){
-            $iCount=0;
-            foreach(['direct', 'regex'] as $sType){
-                if (count($aCfg['redirects'][$sType])){
-                    foreach($aCfg['redirects'][$sType] as $sFrom=>$aTo){
+        ;
+        if (isset($aCfg['redirects'])) {
+            $iCount = 0;
+            foreach (['direct', 'regex'] as $sType) {
+                if (count($aCfg['redirects'][$sType])) {
+                    foreach ($aCfg['redirects'][$sType] as $sFrom => $aTo) {
                         $iCount++;
-                        $sTable.=$sTdFirst
-                            .'<td class="type-'.$sType.'">'.$sType.'</td>'
-                            .'<td class="type-'.$sType.'">'
-                                .($sType == 'direct' 
-                                    ? '<a href="?url=http://'.$sHost.$sFrom.'" title="click to test http://'.$sHost.'/'.$sFrom.'" onclick="showInOverlay(this); return false;">'.$sFrom.'</a>' 
-                                    : $sFrom
-                                )
-                            .'</td>'
-                            .'<td class="http-'.$aTo['code'].'">'.$aTo['code'].'</td>'
-                            .'<td>'.$aIco['url'].' <a href="?url='.$aTo['target'].'" title="click to test '.$aTo['target'].'" onclick="showInOverlay(this); return false;">'.$aTo['target'].'</a></td>'
-                            .'</tr>';
+                        $sTable .= $sTdFirst
+                            . '<td class="type-' . $sType . '">' . $sType . '</td>'
+                            . '<td class="type-' . $sType . '">'
+                            . ($sType == 'direct'
+                                ? '<span style="float: right;">'
+                                . '<a href="?url=http://' . $sHost . $sFrom . '" title="click to test http://' . $sHost . '/' . $sFrom . '" onclick="showInOverlay(this); return false;">http</a> '
+                                . '<a href="?url=https://' . $sHost . $sFrom . '" title="click to test http://' . $sHost . '/' . $sFrom . '" onclick="showInOverlay(this); return false;">https</a> '
+                                . '</span>'
+                                : ''
+                            )
+                            . $sFrom
+                            . '</td>'
+                            . '<td class="http-' . $aTo['code'] . '">' . $aTo['code'] . '</td>'
+                            . '<td>' . $aIco['url'] . ' <a href="?url=' . $aTo['target'] . '" title="click to test ' . $aTo['target'] . '" onclick="showInOverlay(this); return false;">' . $aTo['target'] . '</a></td>'
+                            . '</tr>';
                     }
                 }
-                
+
             }
         } else {
-            // type = alias
-            // $sHtml.='<tr>'.$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <a href="#'.getId($aCfg['target']).'">'.$aCfg['target'].'</a>' : '').'</td></tr>';
-            $sTable.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>';
+            $sTable .= $sTdFirst . '<td></td><td></td><td></td><td>'
+                . (isset($aCfg['target']) ? 'see config for <em>' . $aCfg['target'] . '</em>' : '')
+                . '</td></tr>';
         }
-        
+
     }
 
-    $sTable=$sTable
+    $sTable = $sTable
         ? '<table class="mydatatable"><thead>
         <tr>
-        <th>Host</th>
-        <th>Ip address</th>
-        <th>Setup</th>
-        <th>Type</th>
-        <th>From</th>
-        <th>Code</th>
-        <th>Target</th>
+            <th>Host</th>
+            <th>Ip address</th>
+            <th>Setup</th>
+            <th>Type</th>
+            <th>From</th>
+            <th class="th-code">Code</th>
+            <th>Target</th>
         </tr>
-        </thead><tbody>'.$sTable.'</tbody></table></div>'
+        </thead><tbody>' . $sTable . '</tbody></table></div>'
         . '<br><br>'
-        .'<div class="content legend">'
+        . '<div class="content legend">'
             . '<strong>Legend</strong>:<br>'
             . '<table><tbody>'
-                .'<tr>'
+                . '<tr>'
                 . '<td colspan="2"><br>Icons:</td>'
-                .'</tr>'
-                .'<tr>'
-                    . '<td>'
-                    . $aIco['type_config'].' Domain with config entries<br>'
-                    . $aIco['type_alias'].' Alias pointng to the config of another domain<br>'
-                    . $aIco['url'].' clickable url to show result of a http head request<br>'
-                    . '</td>'
-                    . '<td>'
-                    . $aIco['ip_ok'].' Domain is on the same ip like the redirect tool.<br>'
-                    . $aIco['ip_warn'].' Domain is on another domain (= the config from here does not work because redirect is handled on another system)<br>'
-                    . $aIco['ip_err'].' Hostname was not found in DNS<br>'
-                    . '</td>'
                 . '</tr>'
-                .'<tr>'
+                . '<tr>'
+                . '<td>'
+                . $aIco['type_config'] . ' Domain with config entries<br>'
+                . $aIco['type_alias'] . ' Alias pointng to the config of another domain<br>'
+                . $aIco['url'] . ' clickable url to show result of a http head request<br>'
+                . '</td>'
+                . '<td>'
+                . $aIco['ip_ok'] . ' Domain is on the same ip like the redirect tool.<br>'
+                . $aIco['ip_warn'] . ' Domain is on another domain (= the config from here does not work because redirect is handled on another system)<br>'
+                . $aIco['ip_err'] . ' Hostname was not found in DNS<br>'
+                . '</td>'
+                . '</tr>'
+                . '<tr>'
                 . '<td colspan="2"><br>Redirect status codes:</td>'
-                .'<tr>'
-                    . '<td colspan="2">'
-                        . '301 Moved Permanently - The address is outdated! However, the new address of the resource is still being forwarded.<br>'
-                        . '307 Temporary Redirect - The URL is currently being redirected temporarily (method is retained) - but the old address remains valid.<br>'
-                        . '308 Permanent Redirect - The address is outdated! The new address of the resource is forwarded with the same method.<br>'
-                    . '</td>'
-                    .'</tr>'
-            .'</tbody></table>'
-        .'</div>'
-
-        : '<h3>'.$aIco['welcome'].' Welcome!</h3>
+                . '<tr>'
+                . '<td colspan="2">'
+                . '301 Moved Permanently - The address is outdated! However, the new address of the resource is still being forwarded.<br>'
+                . '307 Temporary Redirect - The URL is currently being redirected temporarily (method is retained) - but the old address remains valid.<br>'
+                . '308 Permanent Redirect - The address is outdated! The new address of the resource is forwarded with the same method.<br>'
+                . '</td>'
+                . '</tr>'
+            . '</tbody></table>'
+        . '</div>'
+
+        : '<h3>' . $aIco['welcome'] . ' Welcome!</h3>
             <p>
                 Thank you for the installation!<br>
                 Now is a good moment to create your first config.
@@ -218,66 +223,61 @@ if (!$oR->isEnabled()){
                 <li>Relaod this page</li>
             </ul>
             <p>
-                See the <a href="'.$oR->urlDocs.'Configuration.html" target="_blank">Docs</a> for details.
+                See the <a href="' . $oR->urlDocs . 'Configuration.html" target="_blank">Docs</a> for details.
             </p>
             '
-        ;
+    ;
 
-    $sHtml.='
-    <!--
-    <h2>'.$aIco['h2_head'].' Http head tester</h2>
-    <div class="content">
-    <form>
-        '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/>
-        <button>Http HEAD</button>
-    </form>
-    </div>
-    <br>
-    -->
-    <h2>'.$aIco['h2_config'].' Domains and their redirects</h2>
-    <div class="content">'
-        
-        /*
-        .'<h2>Config array</h2>
-        <pre>'.print_r($aHosts, 1).'</pre>'
-        */
-        .$sTable
-        ;
+    $sHtml .= '
+    <h2>' . $aIco['h2_config'] . ' Domains and their redirects</h2>
+        <div class="content">'
+        . $sTable
+        . '</div>'
+    ;
 
-    $sErrors = $sErrors 
-        ? '<h2>'.$aIco['h2_err'].' Found errors</h2>'
-            .'<div class="content">'
-                .'<ol class="error">'
-                .$sErrors
-                .'</ol>'
-            .'</div>'
+    $sErrors = $sErrors
+        ? '<!-- <h2>' . $aIco['h2_err'] . ' Found errors</h2> -->'
+        . '<ol class="error">'
+        . $sErrors
+        . '</ol>'
         : ''
-        ;
+    ;
 
-    $sHtml.='<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>'
-        ;
+    $sHtml .= '<footer><a href="' . $oR->urlRepo . '">Source</a> | <a href="' . $oR->urlDocs . '">Docs</a></footer>'
+    ;
 }
 
 // ---------- OUTPUT
 
 ?><!doctype html>
 <html>
-    <head>
-        <title>Redirects</title>
-        <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.css"/>
-        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-        <script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.js"></script>
-        <link rel="stylesheet" href="main.css">
-    </head>
-    <body>
-        <h1><?php echo $aIco['h1']?> <a href="?">Redirects :: admin</a></h1>
 
+<head>
+    <title>Redirects</title>
+
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
+        integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
+        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+
+    <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/2.2.1/css/dataTables.dataTables.min.css" />
+    <script type="text/javascript" src="//cdn.datatables.net/2.2.1/js/dataTables.min.js"></script>
+
+    <link rel="stylesheet" href="main.css">
+</head>
+
+<body>
+    <h1><a href="?"> Redirects :: admin</a></h1>
+
+    <main>
         <?php echo $sErrors . $sHtml; ?>
-        <div id="divoverlay" class="overlay" onclick="this.style.display='none';"></div>
-        
-        <script type="text/javascript" src="functions.js"></script>
-        <script>
-
-        </script>
-    </body>
-</html>
+    </main>
+
+    <div id="divoverlay" class="overlay" onclick="this.style.display='none';"></div>
+
+    <script type="text/javascript" src="functions.js"></script>
+    <script>
+
+    </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public_html/admin/main.css b/public_html/admin/main.css
index 7b23959c2995b327c6ab6692c426817bcac4b9ed..d8fd9f535634c3a1a9373040e24213b387d28175 100644
--- a/public_html/admin/main.css
+++ b/public_html/admin/main.css
@@ -1,47 +1,121 @@
-a{color:royalblue;}
-body{background: #f8f8f8; color: #234; font-family: arial; margin: 0;}
-h1{background:rgba(0,0,0,0.05); margin: 0 0 1em;; padding: 0.5em;}
-h1 a{color:#234; text-decoration: none;}
-h2{background: #d0e0e8;  color:#458; margin: 1em 0 0.5em; border-top: 2px solid #fff; border-left: 5px solid #fff; border-top-left-radius: 0.5em; padding: 0.5em; margin: 0 0 1em;}
-h3{color:#ccc; font-size: 250%}
-
-pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2);}
-
-tr:hover{background: #f4f0f8 !important;}
-footer{background:rgba(0,0,0,0.03); margin-top: 4em; text-align: right;padding: 1em;}
-
-.content{margin: 0 1em;}
-.legend{background: #fff; padding: 1em;}
-.error{background: #fcc; padding: 0.2em 1.5em;}
-.warning{color:#651; background:#fec; padding: 0.2em 1em;}
-
-.cfgtype-alias{color:#89a;}
-.http-301::after{color:#a55; content: ' (Moved Permanently)'}
-.http-307::after{color:#488; content: ' (Temporary Redirect)'}
-.http-308::after{color:#a95; content: ' (Permanent Redirect)'}
-.type-direct{color:#383; }
-.type-regex{color:#838; }
-
-.status{padding: 0.5em 1em; position: relative;top: -0.8em; border-left: 2px solid; border-left_: 1.5em solid;font-size: 125%;}
-.status-ok{color:#080; background:#cec; }
-.status-redirect{color:#651; background:#fec;}
-.status-error{color:#800; background:#ecc;}
-.statuscode{background: rgba(255,255,255,0.8); font-size: 150%;}
-
-.location{background:#cde; border: 1px solid rgba(0,0,0,0.2); font-size: 125%; color:#236;}
-.location::before{content:' > '; color:#800; background-color: #fff;}
-.debug{color:#197;}
-
-.overlay{position: fixed; margin: 0; width: 100%; height: 100%; top: 0; left: 0; background: rgba(0,0,0,0.3);overflow: scroll; display: none;}
-.overlay>div{margin: 3% 10%; background: #f8f8f8; padding: 1em;box-shadow: 0 0 3em #000; }
+:root{
+
+  --col-border-1: #6aa;
+
+  --body-bg: linear-gradient(-10deg, #ddd, #fff, #ddd, #e5e5e5) fixed;
+  --body-color: #335;
+
+  --top-bg: linear-gradient(0deg, #000, #234, #222); 
+  --top-color: #e55;
+  --top-bottom: var(--col-border-1);
+
+  --link-color: #46d;
+
+  --h2-bg: #c8e0e0;
+  --h2-color: #458;
+  --h2-border: var(--col-border-1);
+
+  --h3-color: #ccc;
+
+  --footer-bg: rgba(0,0,0,0.05);
+
+  --box-content-bg: #fff;
+  --box-legend-bg: #fff4e8;
+
+  --txt-error-bg: #ebb;
+  --txt-error-color: #800;
+  --txt-ok-bg: #aeb;
+  --txt-ok-color: #080;
+  --txt-warning-bg: #fda;
+  --txt-warning-color: #651;
+
+  --txt-alias-color: #89a;
+
+  --http-301-color: #a55;
+  --http-307-color: #488;
+  --http-308-color: #a95;
+
+  --type-direct-color:#383;
+  --type-regex-color:#838;
+
+  --txt-statuscode-bg: rgba(255,255,255,0.8);
+
+  --txt-location-bg: #cde;
+  --txt-location-color: #236;
+
+  --txt-location-before-bg: #fff;
+  --txt-debug-color: #197;
+
+  --overlay-bg: rgba(0,0,0,0.3);
+  --overlay-box-bg: #f8f8f8;
+  --overlay-box-shadow: 0 0 3em #000;
+
+  --spin-color1: #ccc;
+  --spin-color2: var(--col-border-1);
+
+}
+
+
+a{color: var(--link-color);}
+
+body {
+	background: var(--body-bg);
+	color: var(--body-color);
+	font-size: 1.2em;
+	font-family: arial;
+	margin: 0;
+	padding: 0;
+	height: 100%,;
+}
+
+h1{margin: 0 0 0.5em;; padding: 0;}
+h1 a{background: var(--top-bg); color: var(--top-color); text-decoration: none;border-bottom: 5px solid var(--top-bottom); display: block; padding: 0.5em;}
+
+h2{background: var(--h2-bg); color: var('--h2-color'); margin: 1em 0 0.5em; border-top: 2px solid var(--h2-border); border-left: 5px solid var(--h2-border); border-top-left-radius: 0.5em; padding: 0.5em; margin: 0;}
+h3{color:var('--h3-color'); font-size: 250%}
+
+pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2); border-bottom-right-radius: 1em;}
+
+.dataTable tr:hover{background: #f4f0f8 !important;}
+footer{background: var(--footer-bg); margin-top: 4em; text-align: right; padding: 1em;}
+main{margin: 0 2em; padding: 0em;}
+
+.content{background: var(--box-content-bg); padding: 1em;}
+.legend{background: var(--box-legend-bg); border-top: 1px dashed; padding: 1em;}
+.error{background: var(--txt-error-bg); color: var(--txt-error-color); padding: 0.2em 0.5em;}
+.warning{background: var(--txt-warning-bg); color: var(--txt-warning-color); }
+
+ol.error{padding-left: 2em;}
+
+.th-code{min-width: 12em;}
+
+.cfgtype-alias{color:var(--txt-alias-color);}
+.http-301::after{color: var(--http-301-color); content: ' (Moved Permanently)'}
+.http-307::after{color:var(--http-307-color); content: ' (Temporary Redirect)'}
+.http-308::after{color:var(--http-308-color); content: ' (Permanent Redirect)'}
+.type-direct{color: var(--type-direct-color); }
+.type-regex{color: var(--type-regex-color); }
+
+.status{padding: 0.5em 1em; position: relative; top: -1.4em; left: -0.9em; border-left: 5px solid; font-size: 125%; border-bottom-right-radius: 0.7em;}
+.status-ok{background: var(--txt-ok-bg); color: var(--txt-ok-color);}
+.status-redirect{background: var(--txt-warning-bg); color: var(--txt-warning-color);}
+.status-error{background: var(--txt-error-bg); color: var(--txt-error-color);}
+.statuscode{background: var(--txt-statuscode-bg); font-size: 150%;}
+
+.location{background:var(--txt-location-bg); font-size: 125%; color:var(--txt-location-color);}
+.location::before{content:' 🌐 '; background-color: var(--txt-location-before-bg);}
+.debug{color:var(--txt-debug-color);}
+
+.overlay{position: fixed; margin: 0; width: 100%; height: 100%; top: 0; left: 0; background: var(--overlay-bg); overflow: scroll; display: none;}
+.overlay>div{margin: 3% 10%; background: var(--overlay-box-bg); padding: 1em;box-shadow: var(--overlay-box-shadow); }
 
 .spin {
     display: inline-block;
     width: 50px;
     height: 50px;
-    border: 3px solid #ccc;
+    border: 3px solid var(--spin-color1);
     border-radius: 50%;
-    border-top-color: #1c87c9;
+    border-top-color: var(--spin-color2);
     animation: spin 1s ease-in-out infinite;
     -webkit-animation: spin 1s ease-in-out infinite;
   }
diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php
index 9a1ed976c69ddc31599ab96b986100258b15b1fd..36e07590543d8724a937691feaf4f623be8c7833 100644
--- a/public_html/classes/redirect.admin.class.php
+++ b/public_html/classes/redirect.admin.class.php
@@ -19,6 +19,7 @@ require_once 'redirect.class.php';
  * 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-04  v1.9  ah  php8 only: typed variables
+ * 2025-01-13  v1.10 ah  fetch curl error
  */
 
 /**
@@ -57,13 +58,15 @@ class redirectadmin extends redirect
     }
 
     /**
-     * Make a single http(s) get request and return the response body
+     * Make a single http(s) get request and return an array with response header, body, curl error info
      * @param string   $url          url to fetch
      * @param boolean  $bHeaderOnly  optional: true=make HEAD request; default: false (=GET)
-     * @return string
+     * @return array
      */
-    public function httpGet(string $url, bool $bHeaderOnly = false): bool|string
+    public function httpGet(string $url, bool $bHeaderOnly = false): array
     {
+        $aResult = [];
+
         $ch = curl_init($url);
         foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) {
             curl_setopt($ch, $sCurlOption, $sCurlValue);
@@ -73,29 +76,46 @@ class redirectadmin extends redirect
             curl_setopt($ch, CURLOPT_NOBODY, 1);
         }
         $res = curl_exec($ch);
+
+        $sHeader = '';
+        $sBody = '';
+        $aResponse = explode("\r\n\r\n", $res, 2);
+        list($sHeader, $sBody) = count($aResponse) > 1
+            ? $aResponse
+            : [$aResponse[0], ''];
+
+        $aResult = [
+            'url' => $url,
+            'response_header' => $sHeader,
+            'response_body' => $sBody,
+            'curlinfo' => curl_getinfo($ch),
+            'curlerrorcode' => curl_errno($ch),
+            'curlerrormsg' => curl_error($ch),
+        ];
+
         curl_close($ch);
-        return ($res);
+        return $aResult;
     }
 
     /**
      * Get html code for a response header of a request
      * 
-     * @param string $sHeader
+     * @param array $aResponse  response array from httpGet() method
      * @return string
      */
-    public function renderHttpResponseHeader(string $sHeader): string
+    public function renderHttpResponseHeader(array $aResponse): string
     {
+        $sHeader=$aResponse['response_header'];
         $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>'
+        // $sReturn.="<pre>".print_r($aResponse, 1)."</pre>";
+
+        if ($aResponse['curlerrorcode']) {
+            $sReturn = '<pre><br>'
+                .'<span class="status status-error">Request failed.<br></span>'
+                .'Curl error #'.$aResponse['curlerrorcode'] .':<br>'
+                . '<strong>'.$aResponse['curlerrormsg'].'</strong><br><br>'
+                .'</pre>'
+                .'🌐 <a href="https://curl.se/libcurl/c/libcurl-errors.html" target="_blank">Curl error codes</a>'
             ;
         } else {
 
diff --git a/readme.md b/readme.md
index da271843ca6810186ea9b715a5dafc14612803c4..f03652a5f7c2a4647b42edde0113de3e76d658a0 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,7 @@
 
-# IML Redirect #
+# IML Redirect
 
-Redirect urls of any domain that points here.
+Redirect tool for multiple domains (that point here).
 
 Author: Axel Hahn; Institute for Medical Education; University of Bern
 
@@ -11,4 +11,16 @@ Author: Axel Hahn; Institute for Medical Education; University of Bern
 
 - - -
 
-![Admin ui](docs/images/admin_ui.png)
\ No newline at end of file
+## Screenshots
+
+Admin web ui with all configured domains and their redirects:
+
+![Admin ui](docs/images/admin_ui.png)
+
+Urls, Redirects and target links can be tested. You see the http reponse header. Redirects will be highlighted.
+
+![Request successful](docs/images/admin_ui_box_http_ok.png)
+
+If a connection fails you get the curl error.
+
+![Connection failed](docs/images/admin_ui_box_connect_failed.png)