浏览代码

[BS5] add host statistics

FreddleSpl0it 3 年之前
父节点
当前提交
a3c0737ba8

+ 1 - 0
data/Dockerfiles/dockerapi/Dockerfile

@@ -8,6 +8,7 @@ RUN apk add --update --no-cache python3 \
   py3-pip \
   py3-pip \
   openssl \
   openssl \
   tzdata \
   tzdata \
+  py3-psutil \
 && pip3 install --upgrade pip \
 && pip3 install --upgrade pip \
   docker \
   docker \
   flask \
   flask \

+ 45 - 0
data/Dockerfiles/dockerapi/dockerapi.py

@@ -6,6 +6,7 @@ from flask import jsonify
 from flask import Response
 from flask import Response
 from flask import request
 from flask import request
 from threading import Thread
 from threading import Thread
+from datetime import datetime
 import docker
 import docker
 import uuid
 import uuid
 import signal
 import signal
@@ -17,6 +18,7 @@ import ssl
 import socket
 import socket
 import subprocess
 import subprocess
 import traceback
 import traceback
+import psutil
 
 
 docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 app = Flask(__name__)
 app = Flask(__name__)
@@ -326,6 +328,48 @@ class container_post(Resource):
         else:
         else:
             return jsonify(type='danger', msg='command did not complete')
             return jsonify(type='danger', msg='command did not complete')
 
 
+class host_stats_get(Resource):
+  def get(self):
+    try:
+      system_time = datetime.now()
+
+      disk_io_before = psutil.disk_io_counters(perdisk=False)
+      net_io_before = psutil.net_io_counters(pernic=False)
+      time.sleep(1)
+      disk_io_after = psutil.disk_io_counters(perdisk=False)
+      net_io_after = psutil.net_io_counters(pernic=False)
+
+      disks_read_per_sec = disk_io_after.read_bytes - disk_io_before.read_bytes
+      disks_write_per_sec = disk_io_after.write_bytes - disk_io_before.write_bytes
+      net_recv_per_sec = net_io_after.bytes_recv - net_io_before.bytes_recv
+      net_sent_per_sec = net_io_after.bytes_sent - net_io_before.bytes_sent
+
+
+      host_stats = {
+        "cpu": {
+          "cores": psutil.cpu_count(),
+          "usage": psutil.cpu_percent()
+        },
+        "memory": {
+          "total": psutil.virtual_memory().total,
+          "usage": psutil.virtual_memory().percent,
+          "swap": psutil.swap_memory()
+        },
+        "disk": {
+          "read_bytes": disks_read_per_sec,
+          "write_bytes": disks_write_per_sec
+        },
+        "network": {
+          "bytes_recv": net_recv_per_sec,
+          "bytes_sent": net_sent_per_sec
+        },
+        "uptime": time.time() - psutil.boot_time(),
+        "system_time": system_time.strftime("%d.%m.%Y %H:%M:%S")
+      }
+      return host_stats
+    except Exception as e:
+      return jsonify(type='danger', msg=str(e))
+
 def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
 def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
 
 
   def recv_socket_data(c_socket, timeout):
   def recv_socket_data(c_socket, timeout):
@@ -406,6 +450,7 @@ def startFlaskAPI():
 api.add_resource(containers_get, '/containers/json')
 api.add_resource(containers_get, '/containers/json')
 api.add_resource(container_get,  '/containers/<string:container_id>/json')
 api.add_resource(container_get,  '/containers/<string:container_id>/json')
 api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
 api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
+api.add_resource(host_stats_get, '/host/stats')
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
   api_thread = Thread(target=startFlaskAPI)
   api_thread = Thread(target=startFlaskAPI)

+ 7 - 0
data/web/debug.php

@@ -44,15 +44,22 @@ foreach ($containers as $container => $container_info) {
   $containers[$container]['State']['StartedAtHR'] = $started;
   $containers[$container]['State']['StartedAtHR'] = $started;
 }
 }
 
 
+// get mailconf data
+$hostname = getenv('MAILCOW_HOSTNAME');
+$timezone = getenv('TZ');
+
 $template = 'debug.twig';
 $template = 'debug.twig';
 $template_data = [
 $template_data = [
   'log_lines' => getenv('LOG_LINES'),
   'log_lines' => getenv('LOG_LINES'),
   'vmail_df' => $vmail_df,
   'vmail_df' => $vmail_df,
+  'hostname' => $hostname,
+  'timezone' => $timezone,
   'solr_status' => $solr_status,
   'solr_status' => $solr_status,
   'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
   'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
   'clamd_status' => $clamd_status,
   'clamd_status' => $clamd_status,
   'containers' => $containers,
   'containers' => $containers,
   'lang_admin' => json_encode($lang['admin']),
   'lang_admin' => json_encode($lang['admin']),
+  'lang_debug' => json_encode($lang['debug']),
   'lang_datatables' => json_encode($lang['datatables']),
   'lang_datatables' => json_encode($lang['datatables']),
 ];
 ];
 
 

+ 19 - 0
data/web/inc/functions.docker.inc.php

@@ -32,6 +32,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
         }
         }
       }
       }
       return false;
       return false;
+    break;
     case 'containers':
     case 'containers':
       curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
       curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
       curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
       curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
@@ -146,5 +147,23 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
         }
         }
       }
       }
     break;
     break;
+    case 'host_stats':
+      curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/host/stats');
+      curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+      curl_setopt($curl, CURLOPT_POST, 0);
+      curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
+      $response = curl_exec($curl);
+      if ($response === false) {
+        $err = curl_error($curl);
+        curl_close($curl);
+        return $err;
+      }
+      else {
+        curl_close($curl);
+        $stats = json_decode($response, true);
+        if (!empty($stats)) return $stats;
+      }
+      return false;
+    break;
   }
   }
 }
 }

+ 239 - 0
data/web/js/site/debug.js

@@ -36,6 +36,15 @@ $(document).ready(function() {
       $(this).text(started_local_date);
       $(this).text(started_local_date);
     }
     }
   });
   });
+
+  // set default ChartJs Font Color
+  Chart.defaults.color = '#999';
+  // create net and disk charts
+  createNetAndDiskChart();
+  // check for new version
+  check_update(mailcow_info.version_tag, mailcow_info.project_url);
+  // update system stats
+  update_stats();
 });
 });
 jQuery(function($){
 jQuery(function($){
   if (localStorage.getItem("current_page") === null) {
   if (localStorage.getItem("current_page") === null) {
@@ -998,3 +1007,233 @@ jQuery(function($){
   onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
   onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
   onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
   onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
 });
 });
+
+
+// update system stats - every 5 seconds if system & container tab is active
+function update_stats(){
+  if (!$('#tab-containers').hasClass('active')) {
+    // tab not active - dont fetch stats - run again in n seconds
+    setTimeout(update_stats, 5000);
+    return;
+  }
+
+  window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) {
+    return response.json();
+  }).then(function(data) {
+    $("#host_date").text(data.system_time);
+    $("#host_uptime").text(formatUptime(data.uptime));
+    $("#host_cpu_cores").text(data.cpu.cores);
+    $("#host_cpu_usage").text(parseInt(data.cpu.usage).toString() + "%");
+    $("#host_memory_total").text((data.memory.total / (1024 ** 3)).toFixed(2).toString() + "GB");
+    $("#host_memory_usage").text(parseInt(data.memory.usage).toString() + "%");
+
+    var net_io_chart = Chart.getChart("net_io_chart");
+    var disk_io_chart = Chart.getChart("disk_io_chart");
+    
+    net_io_chart.data.labels.push(data.system_time.split(" ")[1]);
+    if (net_io_chart.data.labels.length > 20) {
+      net_io_chart.data.labels.shift();
+    }
+    net_io_chart.data.datasets[0].data.push((data.network.bytes_recv / 1024).toFixed(4));
+    net_io_chart.data.datasets[1].data.push((data.network.bytes_sent / 1024).toFixed(4));
+    if (net_io_chart.data.datasets[0].data.length > 20) {
+      net_io_chart.data.datasets[0].data.shift();
+    }
+    if (net_io_chart.data.datasets[1].data.length > 20) {
+      net_io_chart.data.datasets[1].data.shift();
+    }
+
+    disk_io_chart.data.labels.push(data.system_time.split(" ")[1]);
+    if (disk_io_chart.data.labels.length > 20) {
+      disk_io_chart.data.labels.shift();
+    }
+    disk_io_chart.data.datasets[0].data.push((data.disk.read_bytes / 1024).toFixed(4));
+    disk_io_chart.data.datasets[1].data.push((data.disk.write_bytes / 1024).toFixed(4));
+    if (disk_io_chart.data.datasets[0].data.length > 20) {
+      disk_io_chart.data.datasets[0].data.shift();
+    }
+    if (disk_io_chart.data.datasets[1].data.length > 20) {
+      disk_io_chart.data.datasets[1].data.shift();
+    }
+
+    net_io_chart.update();
+    disk_io_chart.update();
+
+    // run again in n seconds
+    setTimeout(update_stats, 5000);
+  });
+}
+// format hosts uptime seconds to readable string
+function formatUptime(seconds){
+  seconds = Number(seconds);
+  var d = Math.floor(seconds / (3600*24));
+  var h = Math.floor(seconds % (3600*24) / 3600);
+  var m = Math.floor(seconds % 3600 / 60);
+  var s = Math.floor(seconds % 60);
+
+  var dFormat = d > 0 ? d + "D " : "";
+  var hFormat = h > 0 ? h + "H " : "";
+  var mFormat = m > 0 ? m + "M " : "";
+  var sFormat = s > 0 ? s + "S" : "";
+  return dFormat + hFormat + mFormat + sFormat;
+} 
+// create network and disk chart
+function createNetAndDiskChart(){
+  var net_io_ctx = document.getElementById("net_io_chart");
+  var disk_io_ctx = document.getElementById("disk_io_chart");
+
+  var dataNet = {
+    labels: [],
+    datasets: [{
+      label: "Recieve",
+      backgroundColor: "rgba(41, 187, 239, 0.3)",
+      borderColor: "rgba(41, 187, 239, 0.6)",
+      color: "#ff0000",
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }, {
+      label: "Sent",
+      backgroundColor: "rgba(239, 60, 41, 0.3)",
+      borderColor: "rgba(239, 60, 41, 0.6)",
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }]
+  };
+  var optionsNet = {
+    scales: {
+      yAxis: {
+        min: 0,
+        grid: {
+            display: false
+        },
+        ticks: {
+          callback: function(i, index, ticks) {
+            // b
+            if (i < 1000) return i.toFixed(2).toString()+' B/s';
+            // b to kb
+            i = i / 1024;
+            if (i < 1000) return i.toFixed(2).toString()+' KB/s';
+            // kb to mb
+            i = i / 1024;
+            if (i < 1000) return i.toFixed(2).toString()+' MB/s';
+            // final mb to gb
+            return (i / 1024).toFixed(2).toString()+' GB/s';
+          }
+        }  
+      },
+      xAxis: {
+        grid: {
+            display: false
+        }  
+      }
+    }
+  };
+
+  var dataDisk = {
+    labels: [],
+    datasets: [{
+      label: "Read",
+      backgroundColor: "rgba(41, 187, 239, 0.3)",
+      borderColor: "rgba(41, 187, 239, 0.6)",
+      color: "#ff0000",
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }, {
+      label: "Write",
+      backgroundColor: "rgba(239, 60, 41, 0.3)",
+      borderColor: "rgba(239, 60, 41, 0.6)",
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }]
+  };
+  var optionsDisk = {
+    scales: {
+      yAxis: {
+        min: 0,
+        grid: {
+            display: false
+        },
+        ticks: {
+          callback: function(i, index, ticks) {
+            // b
+            if (i < 1000) return i.toFixed(2).toString()+' B/s';
+            // b to kb
+            i = i / 1024;
+            if (i < 1000) return i.toFixed(2).toString()+' KB/s';
+            // kb to mb
+            i = i / 1024;
+            if (i < 1000) return i.toFixed(2).toString()+' MB/s';
+            // final mb to gb
+            return (i / 1024).toFixed(2).toString()+' GB/s';
+          }
+        }  
+      },
+      xAxis: {
+        grid: {
+            display: false
+        }  
+      }
+    }
+  };
+
+  
+  var net_io_chart = new Chart(net_io_ctx, {
+    type: 'line',
+    data: dataNet,
+    options: optionsNet
+  });
+  var disk_io_chart = new Chart(disk_io_ctx, {
+    type: 'line',
+    data: dataDisk,
+    options: optionsDisk
+  });
+}
+// check for mailcow updates
+function check_update(current_version, github_repo_url){
+  var github_account = github_repo_url.split("/")[3];
+  var github_repo_name = github_repo_url.split("/")[4];
+
+  // get details about latest release
+  window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) {
+    return response.json();
+  }).then(function(latest_data) {
+    // get details about current release
+    window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) {
+      return response.json();
+    }).then(function(current_data) {
+      // compare releases
+      var date_current = new Date(current_data.created_at);
+      var date_latest = new Date(latest_data.created_at);
+      if (date_latest.getTime() <= date_current.getTime()){
+        // no update available
+        $("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
+        $("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
+      } else {
+        // update available
+        $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
+        $("#mailcow_update").html(
+          `<b>` + lang_debug.update_available + `
+          <a target="_blank" href="https://github.com/`+github_account+`/`+github_repo_name+`/releases/tag/`+latest_data.tag_name+`">`+latest_data.tag_name+`</a></b>`
+        );
+      }
+    }).catch(err => {
+      // err
+      console.log(err);
+      $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
+      $("#mailcow_update").html("<b>Could not check for an Update</b>");
+    });
+  }).catch(err => {
+    // err
+    console.log(err);
+    $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
+    $("#mailcow_update").html("<b>Could not check for an Update</b>");
+  });
+}

+ 27 - 23
data/web/json_api.php

@@ -1474,29 +1474,33 @@ if (isset($_GET['query'])) {
                     'used_percent' => $vmail_df[4]
                     'used_percent' => $vmail_df[4]
                   );
                   );
                   echo json_encode($temp, JSON_UNESCAPED_SLASHES);
                   echo json_encode($temp, JSON_UNESCAPED_SLASHES);
-              break;
-              case "solr":
-                $solr_status = solr_status();
-                $solr_size = ($solr_status['status']['dovecot-fts']['index']['size']);
-                $solr_documents = ($solr_status['status']['dovecot-fts']['index']['numDocs']);
-                if (strtolower(getenv('SKIP_SOLR')) != 'n') {
-                  $solr_enabled = false;
-                }
-                else {
-                  $solr_enabled = true;
-                }
-                echo json_encode(array(
-                  'type' => 'info',
-                  'solr_enabled' => $solr_enabled,
-                  'solr_size' => $solr_size,
-                  'solr_documents' => $solr_documents
-                ));
-              break;
-              case "version":
-                echo json_encode(array(
-                  'version' => $GLOBALS['MAILCOW_GIT_VERSION']
-                ));
-              break;
+                break;
+                case "solr":
+                  $solr_status = solr_status();
+                  $solr_size = ($solr_status['status']['dovecot-fts']['index']['size']);
+                  $solr_documents = ($solr_status['status']['dovecot-fts']['index']['numDocs']);
+                  if (strtolower(getenv('SKIP_SOLR')) != 'n') {
+                    $solr_enabled = false;
+                  }
+                  else {
+                    $solr_enabled = true;
+                  }
+                  echo json_encode(array(
+                    'type' => 'info',
+                    'solr_enabled' => $solr_enabled,
+                    'solr_size' => $solr_size,
+                    'solr_documents' => $solr_documents
+                  ));
+                break;  
+                case "host":
+                  $stats = docker("host_stats");
+                  echo json_encode($stats);
+                break;
+                case "version":
+                  echo json_encode(array(
+                    'version' => $GLOBALS['MAILCOW_GIT_VERSION']
+                  ));
+                break;
               }
               }
             }
             }
           break;
           break;

+ 5 - 0
data/web/lang/lang.de.json

@@ -494,6 +494,7 @@
         "containers_info": "Container-Information",
         "containers_info": "Container-Information",
         "container_running": "Läuft",
         "container_running": "Läuft",
         "container_stopped": "Angehalten",
         "container_stopped": "Angehalten",
+        "current_time": "Systemzeit",
         "disk_usage": "Festplattennutzung",
         "disk_usage": "Festplattennutzung",
         "docs": "Dokumente",
         "docs": "Dokumente",
         "external_logs": "Externe Logs",
         "external_logs": "Externe Logs",
@@ -504,6 +505,7 @@
         "log_info": "<p>mailcow <b>in-memory Logs</b> werden in Redis Listen gespeichert, die maximale Anzahl der Einträge pro Anwendung richtet sich nach LOG_LINES (%d).\r\n  <br>In-memory Logs sind vergänglich und nicht zur ständigen Aufbewahrung bestimmt. Alle Anwendungen, die in-memory protokollieren, schreiben ebenso in den Docker Daemon.\r\n  <br>Das in-memory Protokoll versteht sich als schnelle Übersicht zum Debugging eines Containers, für komplexere Protokolle sollte der Docker Daemon konsultiert werden.</p>\r\n  <p><b>Externe Logs</b> werden via API externer Applikationen bezogen.</p>\r\n  <p><b>Statische Logs</b> sind weitestgehend Aktivitätsprotokolle, die nicht in den Docker Daemon geschrieben werden, jedoch permanent verfügbar sein müssen (ausgeschlossen API Logs).</p>",
         "log_info": "<p>mailcow <b>in-memory Logs</b> werden in Redis Listen gespeichert, die maximale Anzahl der Einträge pro Anwendung richtet sich nach LOG_LINES (%d).\r\n  <br>In-memory Logs sind vergänglich und nicht zur ständigen Aufbewahrung bestimmt. Alle Anwendungen, die in-memory protokollieren, schreiben ebenso in den Docker Daemon.\r\n  <br>Das in-memory Protokoll versteht sich als schnelle Übersicht zum Debugging eines Containers, für komplexere Protokolle sollte der Docker Daemon konsultiert werden.</p>\r\n  <p><b>Externe Logs</b> werden via API externer Applikationen bezogen.</p>\r\n  <p><b>Statische Logs</b> sind weitestgehend Aktivitätsprotokolle, die nicht in den Docker Daemon geschrieben werden, jedoch permanent verfügbar sein müssen (ausgeschlossen API Logs).</p>",
         "login_time": "Zeit",
         "login_time": "Zeit",
         "logs": "Protokolle",
         "logs": "Protokolle",
+        "memory": "Arbeitsspeicher",
         "online_users": "Benutzer online",
         "online_users": "Benutzer online",
         "restart_container": "Neustart",
         "restart_container": "Neustart",
         "service": "Dienst",
         "service": "Dienst",
@@ -515,7 +517,10 @@
         "static_logs": "Statische Logs",
         "static_logs": "Statische Logs",
         "success": "Erfolg",
         "success": "Erfolg",
         "system_containers": "System & Container",
         "system_containers": "System & Container",
+        "timezone": "Zeitzone",
         "uptime": "Uptime",
         "uptime": "Uptime",
+        "update_available": "Es ist ein Update verfügbar",
+        "no_update_available": "Das System ist auf aktuellem Stand",
         "username": "Benutzername"
         "username": "Benutzername"
     },
     },
     "diagnostics": {
     "diagnostics": {

+ 5 - 0
data/web/lang/lang.en.json

@@ -494,6 +494,7 @@
         "containers_info": "Container information",
         "containers_info": "Container information",
         "container_running": "Running",
         "container_running": "Running",
         "container_stopped": "Stopped",
         "container_stopped": "Stopped",
+        "current_time": "System Time",
         "disk_usage": "Disk usage",
         "disk_usage": "Disk usage",
         "docs": "Docs",
         "docs": "Docs",
         "external_logs": "External logs",
         "external_logs": "External logs",
@@ -504,6 +505,7 @@
         "log_info": "<p>mailcow <b>in-memory logs</b> are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\r\n  <br>In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\r\n  <br>The in-memory log type should be used for debugging minor issues with containers.</p>\r\n  <p><b>External logs</b> are collected via API of the given application.</p>\r\n  <p><b>Static logs</b> are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).</p>",
         "log_info": "<p>mailcow <b>in-memory logs</b> are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\r\n  <br>In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\r\n  <br>The in-memory log type should be used for debugging minor issues with containers.</p>\r\n  <p><b>External logs</b> are collected via API of the given application.</p>\r\n  <p><b>Static logs</b> are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).</p>",
         "login_time": "Time",
         "login_time": "Time",
         "logs": "Logs",
         "logs": "Logs",
+        "memory": "Memory",
         "online_users": "Users online",
         "online_users": "Users online",
         "restart_container": "Restart",
         "restart_container": "Restart",
         "service": "Service",
         "service": "Service",
@@ -515,7 +517,10 @@
         "static_logs": "Static logs",
         "static_logs": "Static logs",
         "success": "Success",
         "success": "Success",
         "system_containers": "System & Containers",
         "system_containers": "System & Containers",
+        "timezone": "Timezone",
         "uptime": "Uptime",
         "uptime": "Uptime",
+        "update_available": "There is an update available",
+        "no_update_available": "The System is on the latest version",
         "username": "Username"
         "username": "Username"
     },
     },
     "diagnostics": {
     "diagnostics": {

+ 0 - 33
data/web/templates/admin/tab-config-admins.twig

@@ -89,39 +89,6 @@
         <br>
         <br>
       </div>
       </div>
 
 
-      <legend style="cursor:pointer;margin-top:40px" data-bs-target="#license" unselectable="on" data-bs-toggle="collapse">
-        <i style="font-size:10pt;" class="bi bi-plus-square"></i> {{ lang.admin.guid_and_license }}
-      </legend>
-      <hr />
-      <div id="license" class="collapse">
-        <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
-          <div class="row">
-            <label class="control-label col-sm-3" for="guid">{{ lang.admin.guid }}:</label>
-            <div class="col-sm-9">
-              <div class="input-group">
-                <span class="input-group-text">
-                  <i class="bi bi-suit-heart{% if gal.valid == true %}-fill text-danger{% endif %}"></i>
-                </span>
-                <input type="text" id="guid" class="form-control" value="{{ license_guid }}" readonly>
-              </div>
-              <p class="text-muted">
-                {{ lang.admin.customer_id }}: {{ gal.c|default('?')|raw }} -
-                {{ lang.admin.service_id }}: {{ gal.s|default('?')|raw }} -
-                {{ lang.admin.sal_level }}: {{ gal.m|default('?')|raw }}
-              </p>
-            </div>
-          </div>
-          <div class="row">
-            <div class="offset-sm-3 col-sm-9">
-              <p class="text-muted">{{ lang.admin.license_info|raw }}</p>
-              <div class="btn-group">
-                <button class="btn btn-sm d-block d-sm-inline btn-success" name="license_validate_now" type="submit" href="#">{{ lang.admin.validate_license_now }}</button>
-              </div>
-            </div>
-          </div>
-        </form>
-      </div>
-
       <legend style="cursor:pointer;margin-top:20px" data-bs-target="#admin_api" unselectable="on" data-bs-toggle="collapse">
       <legend style="cursor:pointer;margin-top:20px" data-bs-target="#admin_api" unselectable="on" data-bs-toggle="collapse">
         <i style="font-size:10pt;" class="bi bi-plus-square"></i> API
         <i style="font-size:10pt;" class="bi bi-plus-square"></i> API
       </legend>
       </legend>

+ 120 - 8
data/web/templates/debug.twig

@@ -28,26 +28,125 @@
 <div class="row">
 <div class="row">
   <div class="col-md-12">
   <div class="col-md-12">
     <div class="tab-content" style="padding-top:20px">
     <div class="tab-content" style="padding-top:20px">
-      <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
       <div role="tabpanel" class="tab-pane active" id="tab-containers">
       <div role="tabpanel" class="tab-pane active" id="tab-containers">
+
         <div class="card mb-4">
         <div class="card mb-4">
           <div class="card-header">
           <div class="card-header">
-            <h3 class="card-title">{{ lang.debug.disk_usage }}</h3>
+            <h3 class="card-title">mailcow</h3>
           </div>
           </div>
           <div class="card-body">
           <div class="card-body">
             <div class="row">
             <div class="row">
-              <div class="col-sm-3">
-                <p><i class="bi bi-hdd-fill"></i> {{ vmail_df[0] }}</p>
-                <p>{{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }})</p>
+              <div class="col-sm-12 col-md-4 d-flex flex-column">
+                <img class="img-responsive" alt="mailcow-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}" style="max-height: 200px;">
+                <div style="margin-top: 60px;">
+                  <span class="d-block"><i class="bi bi-hdd-fill"></i> {{ vmail_df[0] }}</span>
+                  <span class="d-block">{{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }})</span>
+                </div>
+                <div class="mt-2 mb-4">
+                  <div class="progress">
+                    <div class="progress-bar bg-info" role="progressbar" style="width:{{ vmail_df[4] }}"></div>
+                  </div>
+                </div>
               </div>
               </div>
-              <div class="col-sm-9">
-                <div class="progress">
-                  <div class="progress-bar bg-info" role="progressbar" style="width:{{ vmail_df[4] }}"></div>
+              <div class="col-sm-12 col-md-8">
+                <div class="table-responsive" style="margin-top: 10px;">
+                  <table class="table table-striped table-condensed">
+                    <tbody>
+                      <tr>
+                        <td>Hostname</td>
+                        <td><div>
+                          <p><b>{{ hostname }}</b></p>
+                        </div></td>
+                      </tr>
+                      <tr>
+                        <td>Version</td>
+                        <td><div>
+                          <p><b>{{ mailcow_info.version_tag }}</b></p>
+                          <p id="mailcow_update"></p> 
+                        </div></td>
+                      </tr>
+                      <tr>
+                        <td>Changelog</td>
+                        <td><a href="{{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">
+                          {{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}
+                        </a></td>
+                      </tr>
+                      <tr>
+                        <td>{{ lang.debug.current_time }}</td>
+                        <td id="host_date">-</td>
+                      </tr>
+                      <tr>
+                        <td>{{ lang.debug.timezone }}</td>
+                        <td>{{ timezone }}</td>
+                      </tr>
+                      <tr>
+                        <td>{{ lang.debug.uptime }}</td>
+                        <td id="host_uptime">-</td>
+                      </tr>
+                      <tr>
+                        <td>CPU</td>
+                        <td>
+                          Cores <span id="host_cpu_cores">-</span><br />
+                          Usage <span id="host_cpu_usage"></span>
+                        </td>
+                      </tr>
+                      <tr>
+                        <td>{{ lang.debug.memory }}</td>
+                        <td>
+                          Total <span id="host_memory_total">-</span><br />
+                          Usage <span id="host_memory_usage"></span>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+
+              <div class="col-sm-6">
+                <canvas id="net_io_chart" width="400" height="200"></canvas>
+              </div>
+              <div class="col-sm-6">
+                <canvas id="disk_io_chart" width="400" height="200"></canvas>
+              </div>
+
+              <div class="col-sm-12">
+                <legend class="mt-4">
+                  {{ lang.admin.guid_and_license }}
+                </legend>
+                <hr />
+                <div id="license">
+                  <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
+                    <div class="row">
+                      <label class="control-label col-sm-3" for="guid">{{ lang.admin.guid }}:</label>
+                      <div class="col-sm-9">
+                        <div class="input-group">
+                          <span class="input-group-text">
+                            <i class="bi bi-suit-heart{% if gal.valid == true %}-fill text-danger{% endif %}"></i>
+                          </span>
+                          <input type="text" id="guid" class="form-control" value="{{ license_guid }}" readonly>
+                        </div>
+                        <p class="text-muted">
+                          {{ lang.admin.customer_id }}: {{ gal.c|default('?')|raw }} -
+                          {{ lang.admin.service_id }}: {{ gal.s|default('?')|raw }} -
+                          {{ lang.admin.sal_level }}: {{ gal.m|default('?')|raw }}
+                        </p>
+                      </div>
+                    </div>
+                    <div class="row">
+                      <div class="offset-sm-3 col-sm-9">
+                        <p class="text-muted">{{ lang.admin.license_info|raw }}</p>
+                        <div class="btn-group">
+                          <button class="btn btn-sm d-block d-sm-inline btn-success" name="license_validate_now" type="submit" href="#">{{ lang.admin.validate_license_now }}</button>
+                        </div>
+                      </div>
+                    </div>
+                  </form>
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
+
         <div class="card mb-4">
         <div class="card mb-4">
           <div class="card-header">
           <div class="card-header">
             <h3 class="card-title">{{ lang.debug.solr_status }}</h3>
             <h3 class="card-title">{{ lang.debug.solr_status }}</h3>
@@ -124,6 +223,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
       <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Postfix
           <div class="card-header d-flex">Postfix
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -139,6 +239,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-ui">
       <div role="tabpanel" class="tab-pane" id="tab-ui">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex"> Mailcow UI
           <div class="card-header d-flex"> Mailcow UI
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -154,6 +255,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-sasl">
       <div role="tabpanel" class="tab-pane" id="tab-sasl">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">SASL
           <div class="card-header d-flex">SASL
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -169,6 +271,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
       <div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Dovecot
           <div class="card-header d-flex">Dovecot
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -184,6 +287,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
       <div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">SOGo
           <div class="card-header d-flex">SOGo
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -199,6 +303,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-netfilter-logs">
       <div role="tabpanel" class="tab-pane" id="tab-netfilter-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Netfilter
           <div class="card-header d-flex">Netfilter
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -214,6 +319,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
       <div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Rspamd history
           <div class="card-header d-flex">Rspamd history
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -234,6 +340,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
       <div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Autodiscover
           <div class="card-header d-flex">Autodiscover
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -249,6 +356,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-watchdog-logs">
       <div role="tabpanel" class="tab-pane" id="tab-watchdog-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Watchdog
           <div class="card-header d-flex">Watchdog
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -264,6 +372,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-acme-logs">
       <div role="tabpanel" class="tab-pane" id="tab-acme-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">ACME
           <div class="card-header d-flex">ACME
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -279,6 +388,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-api-logs">
       <div role="tabpanel" class="tab-pane" id="tab-api-logs">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">API
           <div class="card-header d-flex">API
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -294,6 +404,7 @@
       </div>
       </div>
 
 
       <div role="tabpanel" class="tab-pane" id="tab-api-rl">
       <div role="tabpanel" class="tab-pane" id="tab-api-rl">
+        <div class="debug-log-info">{{ lang.debug.log_info|format(log_lines+1)|raw }}</div>
         <div class="card panel-xs-lg">
         <div class="card panel-xs-lg">
           <div class="card-header d-flex">Ratelimits
           <div class="card-header d-flex">Ratelimits
             <div class="btn-group ms-auto">
             <div class="btn-group ms-auto">
@@ -315,6 +426,7 @@
 
 
 <script type='text/javascript'>
 <script type='text/javascript'>
   var lang = {{ lang_admin|raw }};
   var lang = {{ lang_admin|raw }};
+  var lang_debug = {{ lang_debug|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var csrf_token = '{{ csrf_token }}';
   var csrf_token = '{{ csrf_token }}';
   var log_pagination_size = '{{ log_pagination_size }}';
   var log_pagination_size = '{{ log_pagination_size }}';

+ 1 - 1
docker-compose.yml

@@ -509,7 +509,7 @@ services:
             - watchdog
             - watchdog
 
 
     dockerapi-mailcow:
     dockerapi-mailcow:
-      image: mailcow/dockerapi:1.42
+      image: mailcow/dockerapi:1.43
       security_opt:
       security_opt:
         - label=disable
         - label=disable
       restart: always
       restart: always