public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[BS5] add host statistics
a3c0737b
data/Dockerfiles/dockerapi/Dockerfile | 1 +
data/Dockerfiles/dockerapi/dockerapi.py | 45 +++++
data/web/debug.php | 7 +
data/web/inc/functions.docker.inc.php | 19 ++
data/web/js/site/debug.js | 239 ++++++++++++++++++++++++
data/web/json_api.php | 50 ++---
data/web/lang/lang.de.json | 5 +
data/web/lang/lang.en.json | 5 +
data/web/templates/admin/tab-config-admins.twig | 33 ----
data/web/templates/debug.twig | 128 ++++++++++++-
docker-compose.yml | 2 +-
11 files changed, 469 insertions(+), 65 deletions(-)
Diff
diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile
index 41d4a78f..3768dc4f 100644
--- a/data/Dockerfiles/dockerapi/Dockerfile
+++ b/data/Dockerfiles/dockerapi/Dockerfile
@@ -8,6 +8,7 @@ RUN apk add --update --no-cache python3 \
py3-pip \
openssl \
tzdata \
+ py3-psutil \
&& pip3 install --upgrade pip \
docker \
flask \
diff --git a/data/Dockerfiles/dockerapi/dockerapi.py b/data/Dockerfiles/dockerapi/dockerapi.py
index 20e9d0e0..314c1d65 100644
--- a/data/Dockerfiles/dockerapi/dockerapi.py
+++ b/data/Dockerfiles/dockerapi/dockerapi.py
@@ -6,6 +6,7 @@ from flask import jsonify
from flask import Response
from flask import request
from threading import Thread
+from datetime import datetime
import docker
import uuid
import signal
@@ -17,6 +18,7 @@ import ssl
import socket
import subprocess
import traceback
+import psutil
docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
app = Flask(__name__)
@@ -326,6 +328,48 @@ class container_post(Resource):
else:
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 recv_socket_data(c_socket, timeout):
@@ -406,6 +450,7 @@ def startFlaskAPI():
api.add_resource(containers_get, '/containers/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(host_stats_get, '/host/stats')
if __name__ == '__main__':
api_thread = Thread(target=startFlaskAPI)
diff --git a/data/web/debug.php b/data/web/debug.php
index 9cee06b9..e263e181 100644
--- a/data/web/debug.php
+++ b/data/web/debug.php
@@ -44,15 +44,22 @@ foreach ($containers as $container => $container_info) {
$containers[$container]['State']['StartedAtHR'] = $started;
}
+// get mailconf data
+$hostname = getenv('MAILCOW_HOSTNAME');
+$timezone = getenv('TZ');
+
$template = 'debug.twig';
$template_data = [
'log_lines' => getenv('LOG_LINES'),
'vmail_df' => $vmail_df,
+ 'hostname' => $hostname,
+ 'timezone' => $timezone,
'solr_status' => $solr_status,
'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
'clamd_status' => $clamd_status,
'containers' => $containers,
'lang_admin' => json_encode($lang['admin']),
+ 'lang_debug' => json_encode($lang['debug']),
'lang_datatables' => json_encode($lang['datatables']),
];
diff --git a/data/web/inc/functions.docker.inc.php b/data/web/inc/functions.docker.inc.php
index e47f5e2f..260f0a41 100644
--- a/data/web/inc/functions.docker.inc.php
+++ b/data/web/inc/functions.docker.inc.php
@@ -32,6 +32,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
}
}
return false;
+ break;
case 'containers':
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
@@ -146,5 +147,23 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
}
}
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;
}
}
diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js
index 7c4da192..15653252 100644
--- a/data/web/js/site/debug.js
+++ b/data/web/js/site/debug.js
@@ -36,6 +36,15 @@ $(document).ready(function() {
$(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($){
if (localStorage.getItem("current_page") === null) {
@@ -998,3 +1007,233 @@ jQuery(function($){
onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
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>");
+ });
+}
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 8db4ef89..688cd63d 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -1474,29 +1474,33 @@ if (isset($_GET['query'])) {
'used_percent' => $vmail_df[4]
);
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;
diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json
index 36d257e9..afe3053a 100644
--- a/data/web/lang/lang.de.json
+++ b/data/web/lang/lang.de.json
@@ -494,6 +494,7 @@
"containers_info": "Container-Information",
"container_running": "Läuft",
"container_stopped": "Angehalten",
+ "current_time": "Systemzeit",
"disk_usage": "Festplattennutzung",
"docs": "Dokumente",
"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>",
"login_time": "Zeit",
"logs": "Protokolle",
+ "memory": "Arbeitsspeicher",
"online_users": "Benutzer online",
"restart_container": "Neustart",
"service": "Dienst",
@@ -515,7 +517,10 @@
"static_logs": "Statische Logs",
"success": "Erfolg",
"system_containers": "System & Container",
+ "timezone": "Zeitzone",
"uptime": "Uptime",
+ "update_available": "Es ist ein Update verfügbar",
+ "no_update_available": "Das System ist auf aktuellem Stand",
"username": "Benutzername"
},
"diagnostics": {
diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json
index 47b664b0..23c03d6b 100644
--- a/data/web/lang/lang.en.json
+++ b/data/web/lang/lang.en.json
@@ -494,6 +494,7 @@
"containers_info": "Container information",
"container_running": "Running",
"container_stopped": "Stopped",
+ "current_time": "System Time",
"disk_usage": "Disk usage",
"docs": "Docs",
"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>",
"login_time": "Time",
"logs": "Logs",
+ "memory": "Memory",
"online_users": "Users online",
"restart_container": "Restart",
"service": "Service",
@@ -515,7 +517,10 @@
"static_logs": "Static logs",
"success": "Success",
"system_containers": "System & Containers",
+ "timezone": "Timezone",
"uptime": "Uptime",
+ "update_available": "There is an update available",
+ "no_update_available": "The System is on the latest version",
"username": "Username"
},
"diagnostics": {
diff --git a/data/web/templates/admin/tab-config-admins.twig b/data/web/templates/admin/tab-config-admins.twig
index e9018683..1d903b56 100644
--- a/data/web/templates/admin/tab-config-admins.twig
+++ b/data/web/templates/admin/tab-config-admins.twig
@@ -89,39 +89,6 @@
<br>
</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">
<i style="font-size:10pt;" class="bi bi-plus-square"></i> API
</legend>
diff --git a/data/web/templates/debug.twig b/data/web/templates/debug.twig
index 53db5194..732d5cbc 100644
--- a/data/web/templates/debug.twig
+++ b/data/web/templates/debug.twig
@@ -28,26 +28,125 @@
<div class="row">
<div class="col-md-12">
<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 class="card mb-4">
<div class="card-header">
- <h3 class="card-title">{{ lang.debug.disk_usage }}</h3>
+ <h3 class="card-title">mailcow</h3>
</div>
<div class="card-body">
<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 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 class="card mb-4">
<div class="card-header">
<h3 class="card-title">{{ lang.debug.solr_status }}</h3>
@@ -124,6 +223,7 @@
</div>
<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-header d-flex">Postfix
<div class="btn-group ms-auto">
@@ -139,6 +239,7 @@
</div>
<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-header d-flex"> Mailcow UI
<div class="btn-group ms-auto">
@@ -154,6 +255,7 @@
</div>
<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-header d-flex">SASL
<div class="btn-group ms-auto">
@@ -169,6 +271,7 @@
</div>
<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-header d-flex">Dovecot
<div class="btn-group ms-auto">
@@ -184,6 +287,7 @@
</div>
<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-header d-flex">SOGo
<div class="btn-group ms-auto">
@@ -199,6 +303,7 @@
</div>
<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-header d-flex">Netfilter
<div class="btn-group ms-auto">
@@ -214,6 +319,7 @@
</div>
<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-header d-flex">Rspamd history
<div class="btn-group ms-auto">
@@ -234,6 +340,7 @@
</div>
<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-header d-flex">Autodiscover
<div class="btn-group ms-auto">
@@ -249,6 +356,7 @@
</div>
<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-header d-flex">Watchdog
<div class="btn-group ms-auto">
@@ -264,6 +372,7 @@
</div>
<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-header d-flex">ACME
<div class="btn-group ms-auto">
@@ -279,6 +388,7 @@
</div>
<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-header d-flex">API
<div class="btn-group ms-auto">
@@ -294,6 +404,7 @@
</div>
<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-header d-flex">Ratelimits
<div class="btn-group ms-auto">
@@ -315,6 +426,7 @@
<script type='text/javascript'>
var lang = {{ lang_admin|raw }};
+ var lang_debug = {{ lang_debug|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var log_pagination_size = '{{ log_pagination_size }}';
diff --git a/docker-compose.yml b/docker-compose.yml
index e3b08637..a3fe7bcd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -509,7 +509,7 @@ services:
- watchdog
dockerapi-mailcow:
- image: mailcow/dockerapi:1.42
+ image: mailcow/dockerapi:1.43
security_opt:
- label=disable
restart: always