Browse Source

Fix timestamps not sorting in datatables

Timestamps retrieved from the API were always converted to a browser
local format. The format specified for moment.js added in
5160eff29446ba94329116a6ed3787400b03e461 did not work because of this.

Additionally, the format specified used `dd` which looks for two letter
days, such as "Mo", "Tu", "We", etc. Furthermore, `mm` is used for
minutes, not months.

Because the locale formatted datetime can vary a lot, it is not easy to
get this into moment.js to enable the sorting of datetimes in the
datatables. In other words, there is no conversion from an
`Intl.DateTimeFormat` specifier string to moment.js. Adding many
`$.fn.dataTable.moment(format);` with different `format`s is not useful.

I have fixed this rewriting how the timestamps from the API are added
to the tables. It still uses the locale of the browser, because not
everyone wants to use ISO 8601, but no longer requires moment.js (which
has been removed).

Two data attributes are added to the `td`s of the timestamps:
- `data-order`
- `data-sort`

The values of these are the timestamps as returned by the server, which
are very easily sorted (as they are just UNIX timestamps). Then, when
creating the cell in the table, it will be converted to what the locale
of the browser specified (this has not changed).
Tom Udding 2 years ago
parent
commit
240b2c63f6

+ 0 - 0
data/web/js/build/005-datatables.js → data/web/js/build/004-datatables.js


File diff suppressed because it is too large
+ 0 - 5
data/web/js/build/004-moment.min.js


+ 0 - 0
data/web/js/build/007-notifications.min.js → data/web/js/build/005-notifications.min.js


+ 0 - 70
data/web/js/build/006-datetime-moment.js

@@ -1,70 +0,0 @@
-/**
- * This plug-in for DataTables represents the ultimate option in extensibility
- * for sorting date / time strings correctly. It uses
- * [Moment.js](http://momentjs.com) to create automatic type detection and
- * sorting plug-ins for DataTables based on a given format. This way, DataTables
- * will automatically detect your temporal information and sort it correctly.
- *
- * For usage instructions, please see the DataTables blog
- * post that [introduces it](//datatables.net/blog/2014-12-18).
- *
- * @name Ultimate Date / Time sorting
- * @summary Sort date and time in any format using Moment.js
- * @author [Allan Jardine](//datatables.net)
- * @depends DataTables 1.10+, Moment.js 1.7+
- *
- * @example
- *    $.fn.dataTable.moment( 'HH:mm MMM D, YY' );
- *    $.fn.dataTable.moment( 'dddd, MMMM Do, YYYY' );
- *
- *    $('#example').DataTable();
- */
-
-(function (factory) {
-	if (typeof define === "function" && define.amd) {
-		define(["jquery", "moment", "datatables.net"], factory);
-	} else {
-		factory(jQuery, moment);
-	}
-}(function ($, moment) {
-
-function strip (d) {
-	if ( typeof d === 'string' ) {
-		// Strip HTML tags and newline characters if possible
-		d = d.replace(/(<.*?>)|(\r?\n|\r)/g, '');
-
-		// Strip out surrounding white space
-		d = d.trim();
-	}
-
-	return d;
-}
-
-$.fn.dataTable.moment = function ( format, locale, reverseEmpties ) {
-	var types = $.fn.dataTable.ext.type;
-
-	// Add type detection
-	types.detect.unshift( function ( d ) {
-		d = strip(d);
-
-		// Null and empty values are acceptable
-		if ( d === '' || d === null ) {
-			return 'moment-'+format;
-		}
-
-		return moment( d, format, locale, true ).isValid() ?
-			'moment-'+format :
-			null;
-	} );
-
-	// Add sorting method - use an integer for the sorting
-	types.order[ 'moment-'+format+'-pre' ] = function ( d ) {
-		d = strip(d);
-		
-		return !moment(d, format, locale, true).isValid() ?
-			(reverseEmpties ? -Infinity : Infinity) :
-			parseInt( moment( d, format, locale, true ).format( 'x' ), 10 );
-	};
-};
-
-}));

+ 0 - 0
data/web/js/build/008-formcache.min.js → data/web/js/build/006-formcache.min.js


+ 0 - 0
data/web/js/build/009-chart.js → data/web/js/build/007-chart.js


+ 0 - 0
data/web/js/build/010-chartjs-plugin-datalabels.js → data/web/js/build/008-chartjs-plugin-datalabels.js


+ 0 - 0
data/web/js/build/011-numberedtextarea.min.js → data/web/js/build/009-numberedtextarea.min.js


+ 0 - 0
data/web/js/build/012-sha1.min.js → data/web/js/build/010-sha1.min.js


+ 0 - 0
data/web/js/build/013-api.js → data/web/js/build/011-api.js


+ 0 - 0
data/web/js/build/014-markdown-it.min.js → data/web/js/build/012-markdown-it.min.js


+ 353 - 354
data/web/js/build/015-mailcow.js → data/web/js/build/013-mailcow.js

@@ -1,354 +1,353 @@
-$(document).ready(function() {
-  // mailcow alert box generator
-  window.mailcow_alert_box = function(message, type) {
-    msg = $('<span/>').text(message).text();
-    if (type == 'danger' || type == 'info') {
-      auto_hide = 0;
-      $('#' + localStorage.getItem("add_modal")).modal('show');
-      localStorage.removeItem("add_modal");
-    } else {
-      auto_hide = 5000;
-    }
-    $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
-  }
-
-  $(".generate_password").click(function( event ) {
-    event.preventDefault();
-    $('[data-hibp]').trigger('input');
-    if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
-      var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
-    }
-    else {
-      var random_passwd = GPW.pronounceable(8)
-    }
-    $(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
-    $(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
-  });
-  function str_rot13(str) {
-    return (str + '').replace(/[a-z]/gi, function(s){
-      return String.fromCharCode(s.charCodeAt(0) + (s.toLowerCase() < 'n' ? 13 : -13))
-    })
-  }
-  $(".rot-enc").html(function(){
-    return str_rot13($(this).html())
-  });
-  // https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
-  function shake(div,interval,distance,times) {
-      if(typeof interval === 'undefined') {
-        interval = 100;
-      }
-      if(typeof distance === 'undefined') {
-        distance = 10;
-      }
-      if(typeof times === 'undefined') {
-        times = 4;
-      }
-    $(div).css('position','relative');
-    for(var iter=0;iter<(times+1);iter++){
-      $(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
-    }
-    $(div).animate({ left: 0},interval);
-  } 
-
-  // form cache
-  $('[data-cached-form="true"]').formcache({key: $(this).data('id')});
-
-  //  tooltips
-  $(function () {
-    $('[data-bs-toggle="tooltip"]').tooltip()
-  });
-
-  // remember last navigation pill
-  (function () {
-    'use strict';
-      // remember desktop tabs
-      $('button[data-bs-toggle="tab"]').on('click', function (e) {
-        if ($(this).data('dont-remember') == 1) {
-          return true;
-        }
-        var id = $(this).parents('[role="tablist"]').attr('id');
-        var key = 'lastTag';
-        if (id) {
-          key += ':' + id;
-        }
-
-        var tab_id = $(e.target).attr('data-bs-target').substring(1);
-        localStorage.setItem(key, tab_id);
-      });
-      // remember mobile tabs
-      $('button[data-bs-target^="#collapse-tab-"]').on('click', function (e) {
-        // only remember tab if its being opened
-        if ($(this).hasClass('collapsed')) return false;
-        var tab_id = $(this).closest('div[role="tabpanel"]').attr('id');
-
-        if ($(this).data('dont-remember') == 1) {
-          return true;
-        }
-        var id = $(this).parents('[role="tablist"]').attr('id');;
-        var key = 'lastTag';
-        if (id) {
-          key += ':' + id;
-        }
-
-        localStorage.setItem(key, tab_id);
-      });
-      // open last tab
-      $('[role="tablist"]').each(function (idx, elem) {
-        var id = $(elem).attr('id');
-        var key = 'lastTag';
-        if (id) {
-          key += ':' + id;
-        }
-        var lastTab = localStorage.getItem(key);
-        if (lastTab) {
-          $('[data-bs-target="#' + lastTab + '"]').click();
-          var tab = $('[id^="' + lastTab + '"]');
-          $(tab).find('.card-body.collapse').collapse('show');
-        }
-      });
-  })();
-
-  // IE fix to hide scrollbars when table body is empty
-  $('tbody').filter(function (index) {
-    return $(this).children().length < 1;
-  }).remove();
-
-  // selectpicker
-  $('select').selectpicker({
-    'styleBase': 'btn btn-xs-lg',
-    'noneSelectedText': lang_footer.nothing_selected
-  });
-
-  // haveibeenpwned and passwd policy
-  $.ajax({
-    url: '/api/v1/get/passwordpolicy/html',
-    type: 'GET',
-    success: function(res) {
-      $(".hibp-out").after(res);
-    }
-  });
-  $('[data-hibp]').after('<p class="small haveibeenpwned"><i class="bi bi-shield-fill-exclamation"></i> ' + lang_footer.hibp_check + '</p><span class="hibp-out"></span>');
-  $('[data-hibp]').on('input', function() {
-    out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');
-  });
-  $('.haveibeenpwned:not(.task-running)').on('click', function() {
-    var hibp_field = $(this)
-    $(hibp_field).addClass('task-running');
-    var hibp_result = $(hibp_field).next('.hibp-out')
-    var password_field = $(this).prev('[data-hibp]')
-    if ($(password_field).val() == '') {
-      shake(password_field);
-    }
-    else {
-      $(hibp_result).attr('class', 'hibp-out badge fs-5 bg-info');
-      $(hibp_result).text(lang_footer.loading);
-      var password_digest = $.sha1($(password_field).val())
-      var digest_five = password_digest.substring(0, 5).toUpperCase();
-      var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five;
-      var compl_digest = password_digest.substring(5, 41).toUpperCase();
-      $.ajax({
-        url: queryURL,
-        type: 'GET',
-        success: function(res) {
-          if (res.search(compl_digest) > -1){
-            $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-danger');
-            $(hibp_result).text(lang_footer.hibp_nok)
-          } else {
-            $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-success');
-            $(hibp_result).text(lang_footer.hibp_ok)
-          }
-          $(hibp_field).removeClass('task-running');
-        },
-        error: function(xhr, status, error) {
-          $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-warning');
-          $(hibp_result).text('API error: ' + xhr.responseText)
-          $(hibp_field).removeClass('task-running');
-        }
-      });
-    }
-  });
-
-  // Disable disallowed inputs
-  $('[data-acl="0"]').each(function(event){
-    if ($(this).is("a")) {
-      $(this).removeAttr("data-bs-toggle");
-      $(this).removeAttr("data-bs-target");
-      $(this).removeAttr("data-action");
-      $(this).click(function(event) {
-        event.preventDefault();
-      });
-    }
-    if ($(this).is("select")) {
-      $(this).selectpicker('destroy');
-      $(this).replaceWith(function() {
-        return '<label class="control-label"><b>' + this.innerText + '</b></label>';
-      });
-    }
-    if ($(this).hasClass('btn-group')) {
-      $(this).find('a').each(function(){
-        $(this).removeClass('dropdown-toggle')
-          .removeAttr('data-bs-toggle')
-          .removeAttr('data-bs-target')
-          .removeAttr('data-action')
-          .removeAttr('id')
-          .attr("disabled", true);
-        $(this).click(function(event) {
-          event.preventDefault();
-          return;
-        });
-      });
-      $(this).find('button').each(function() {
-        $(this).attr("disabled", true);
-      });
-    } else if ($(this).hasClass('input-group')) {
-      $(this).find('input').each(function() {
-        $(this).removeClass('dropdown-toggle')
-          .removeAttr('data-bs-toggle')
-          .attr("disabled", true);
-        $(this).click(function(event) {
-          event.preventDefault();
-        });
-      });
-      $(this).find('button').each(function() {
-        $(this).attr("disabled", true);
-      });
-    } else if ($(this).hasClass('form-group')) {
-      $(this).find('input').each(function() {
-        $(this).attr("disabled", true);
-      });
-    } else if ($(this).hasClass('btn')) {
-      $(this).attr("disabled", true);
-    } else if ($(this).attr('data-provide') == 'slider') {
-      $(this).attr('disabled', true);
-    } else if ($(this).is(':checkbox')) {
-      $(this).attr("disabled", true);
-    }
-    $(this).data("toggle", "tooltip");
-    $(this).attr("title", lang_acl.prohibited);
-    $(this).tooltip();
-  });
-
-  // disable submit after submitting form (not API driven buttons)
-  $('form').submit(function() {
-    if ($('form button[type="submit"]').data('submitted') == '1') {
-      return false;
-    } else {
-      $(this).find('button[type="submit"]').first().text(lang_footer.loading);
-      $('form button[type="submit"]').attr('data-submitted', '1');
-      function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
-      $(document).on("keydown", disableF5);
-    }
-  });
-  // Textarea line numbers
-  $(".textarea-code").numberedtextarea({allowTabChar: true});
-  // trigger container restart
-  $('#RestartContainer').on('show.bs.modal', function(e) {
-    var container = $(e.relatedTarget).data('container');
-    $('#containerName').text(container);
-    $('#triggerRestartContainer').click(function(){
-      $(this).prop("disabled",true);
-      $(this).html('<div class="spinner-border text-white" role="status"><span class="visually-hidden">Loading...</span></div>');
-      $('#statusTriggerRestartContainer').html(lang_footer.restarting_container);
-      $.ajax({
-        method: 'get',
-        url: '/inc/ajax/container_ctrl.php',
-        timeout: docker_timeout,
-        data: {
-        'service': container,
-        'action': 'restart'
-        }
-      })
-      .always( function (data, status) {
-        $('#statusTriggerRestartContainer').append(data);
-        var htmlResponse = $.parseHTML(data)
-        if ($(htmlResponse).find('span').hasClass('text-success')) {
-          $('#triggerRestartContainer').html('<i class="bi bi-check-lg"></i> ');
-          setTimeout(function(){
-            $('#RestartContainer').modal('toggle');
-            window.location = window.location.href.split("#")[0];
-          }, 1200);
-        } else {
-          $('#triggerRestartContainer').html('<i class="bi bi-slash-lg"></i> ');
-        }
-      })
-    });
-  })
-
-  // Jquery Datatables, enable responsive plugin and date sort plugin
-  $.extend($.fn.dataTable.defaults, {
-    responsive: true
-  });
-  $.fn.dataTable.moment('dd:mm:YYYY');
-
-  // tag boxes
-  $('.tag-box .tag-add').click(function(){
-    addTag(this);
-  });
-  $(".tag-box .tag-input").keydown(function (e) {
-    if (e.which == 13){
-      e.preventDefault();
-      addTag(this);
-    } 
-  });
-
-  // Dark Mode Loader
-  $('#dark-mode-toggle').click(toggleDarkMode);
-  if ($('#dark-mode-theme').length) {
-    $('#dark-mode-toggle').prop('checked', true);
-    if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
-    if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
-  }
-  function toggleDarkMode(){
-    if($('#dark-mode-theme').length){
-      $('#dark-mode-theme').remove();
-      $('#dark-mode-toggle').prop('checked', false);
-      if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
-      if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
-      localStorage.setItem('theme', 'light');
-    }else{
-      $('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
-      $('#dark-mode-toggle').prop('checked', true);
-      if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
-      if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
-      localStorage.setItem('theme', 'dark');
-    }
-  }
-});
-
-
-// https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
-function escapeHtml(n){var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
-function unescapeHtml(t){var n={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&#x2F;":"/","&#x60;":"`","&#x3D;":"="};return String(t).replace(/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F|&#x60|&#x3D;/g,function(t){return n[t]})}
-
-function addTag(tagAddElem, tag = null){
-  var tagboxElem = $(tagAddElem).parent();
-  var tagInputElem = $(tagboxElem).find(".tag-input")[0];
-  var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
-
-  if (!tag)
-    tag = $(tagInputElem).val();
-  if (!tag) return;
-  var value_tags = [];
-  try {
-    value_tags = JSON.parse($(tagValuesElem).val());
-  } catch {}
-  if (!Array.isArray(value_tags)) value_tags = [];
-  if (value_tags.includes(tag)) return;
-
-  $('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(tag) + '</span>').insertBefore('.tag-input').click(function(){
-    var del_tag = unescapeHtml($(this).text());
-    var del_tags = [];
-    try {
-      del_tags = JSON.parse($(tagValuesElem).val());
-    } catch {}
-    if (Array.isArray(del_tags)){
-      del_tags.splice(del_tags.indexOf(del_tag), 1);
-      $(tagValuesElem).val(JSON.stringify(del_tags));
-    }
-    $(this).remove();
-  });
-
-  value_tags.push(tag);
-  $(tagValuesElem).val(JSON.stringify(value_tags));
-  $(tagInputElem).val('');
-}
+$(document).ready(function() {
+  // mailcow alert box generator
+  window.mailcow_alert_box = function(message, type) {
+    msg = $('<span/>').text(message).text();
+    if (type == 'danger' || type == 'info') {
+      auto_hide = 0;
+      $('#' + localStorage.getItem("add_modal")).modal('show');
+      localStorage.removeItem("add_modal");
+    } else {
+      auto_hide = 5000;
+    }
+    $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
+  }
+
+  $(".generate_password").click(function( event ) {
+    event.preventDefault();
+    $('[data-hibp]').trigger('input');
+    if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
+      var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
+    }
+    else {
+      var random_passwd = GPW.pronounceable(8)
+    }
+    $(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
+    $(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
+  });
+  function str_rot13(str) {
+    return (str + '').replace(/[a-z]/gi, function(s){
+      return String.fromCharCode(s.charCodeAt(0) + (s.toLowerCase() < 'n' ? 13 : -13))
+    })
+  }
+  $(".rot-enc").html(function(){
+    return str_rot13($(this).html())
+  });
+  // https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
+  function shake(div,interval,distance,times) {
+      if(typeof interval === 'undefined') {
+        interval = 100;
+      }
+      if(typeof distance === 'undefined') {
+        distance = 10;
+      }
+      if(typeof times === 'undefined') {
+        times = 4;
+      }
+    $(div).css('position','relative');
+    for(var iter=0;iter<(times+1);iter++){
+      $(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
+    }
+    $(div).animate({ left: 0},interval);
+  }
+
+  // form cache
+  $('[data-cached-form="true"]').formcache({key: $(this).data('id')});
+
+  //  tooltips
+  $(function () {
+    $('[data-bs-toggle="tooltip"]').tooltip()
+  });
+
+  // remember last navigation pill
+  (function () {
+    'use strict';
+      // remember desktop tabs
+      $('button[data-bs-toggle="tab"]').on('click', function (e) {
+        if ($(this).data('dont-remember') == 1) {
+          return true;
+        }
+        var id = $(this).parents('[role="tablist"]').attr('id');
+        var key = 'lastTag';
+        if (id) {
+          key += ':' + id;
+        }
+
+        var tab_id = $(e.target).attr('data-bs-target').substring(1);
+        localStorage.setItem(key, tab_id);
+      });
+      // remember mobile tabs
+      $('button[data-bs-target^="#collapse-tab-"]').on('click', function (e) {
+        // only remember tab if its being opened
+        if ($(this).hasClass('collapsed')) return false;
+        var tab_id = $(this).closest('div[role="tabpanel"]').attr('id');
+
+        if ($(this).data('dont-remember') == 1) {
+          return true;
+        }
+        var id = $(this).parents('[role="tablist"]').attr('id');;
+        var key = 'lastTag';
+        if (id) {
+          key += ':' + id;
+        }
+
+        localStorage.setItem(key, tab_id);
+      });
+      // open last tab
+      $('[role="tablist"]').each(function (idx, elem) {
+        var id = $(elem).attr('id');
+        var key = 'lastTag';
+        if (id) {
+          key += ':' + id;
+        }
+        var lastTab = localStorage.getItem(key);
+        if (lastTab) {
+          $('[data-bs-target="#' + lastTab + '"]').click();
+          var tab = $('[id^="' + lastTab + '"]');
+          $(tab).find('.card-body.collapse').collapse('show');
+        }
+      });
+  })();
+
+  // IE fix to hide scrollbars when table body is empty
+  $('tbody').filter(function (index) {
+    return $(this).children().length < 1;
+  }).remove();
+
+  // selectpicker
+  $('select').selectpicker({
+    'styleBase': 'btn btn-xs-lg',
+    'noneSelectedText': lang_footer.nothing_selected
+  });
+
+  // haveibeenpwned and passwd policy
+  $.ajax({
+    url: '/api/v1/get/passwordpolicy/html',
+    type: 'GET',
+    success: function(res) {
+      $(".hibp-out").after(res);
+    }
+  });
+  $('[data-hibp]').after('<p class="small haveibeenpwned"><i class="bi bi-shield-fill-exclamation"></i> ' + lang_footer.hibp_check + '</p><span class="hibp-out"></span>');
+  $('[data-hibp]').on('input', function() {
+    out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');
+  });
+  $('.haveibeenpwned:not(.task-running)').on('click', function() {
+    var hibp_field = $(this)
+    $(hibp_field).addClass('task-running');
+    var hibp_result = $(hibp_field).next('.hibp-out')
+    var password_field = $(this).prev('[data-hibp]')
+    if ($(password_field).val() == '') {
+      shake(password_field);
+    }
+    else {
+      $(hibp_result).attr('class', 'hibp-out badge fs-5 bg-info');
+      $(hibp_result).text(lang_footer.loading);
+      var password_digest = $.sha1($(password_field).val())
+      var digest_five = password_digest.substring(0, 5).toUpperCase();
+      var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five;
+      var compl_digest = password_digest.substring(5, 41).toUpperCase();
+      $.ajax({
+        url: queryURL,
+        type: 'GET',
+        success: function(res) {
+          if (res.search(compl_digest) > -1){
+            $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-danger');
+            $(hibp_result).text(lang_footer.hibp_nok)
+          } else {
+            $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-success');
+            $(hibp_result).text(lang_footer.hibp_ok)
+          }
+          $(hibp_field).removeClass('task-running');
+        },
+        error: function(xhr, status, error) {
+          $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-warning');
+          $(hibp_result).text('API error: ' + xhr.responseText)
+          $(hibp_field).removeClass('task-running');
+        }
+      });
+    }
+  });
+
+  // Disable disallowed inputs
+  $('[data-acl="0"]').each(function(event){
+    if ($(this).is("a")) {
+      $(this).removeAttr("data-bs-toggle");
+      $(this).removeAttr("data-bs-target");
+      $(this).removeAttr("data-action");
+      $(this).click(function(event) {
+        event.preventDefault();
+      });
+    }
+    if ($(this).is("select")) {
+      $(this).selectpicker('destroy');
+      $(this).replaceWith(function() {
+        return '<label class="control-label"><b>' + this.innerText + '</b></label>';
+      });
+    }
+    if ($(this).hasClass('btn-group')) {
+      $(this).find('a').each(function(){
+        $(this).removeClass('dropdown-toggle')
+          .removeAttr('data-bs-toggle')
+          .removeAttr('data-bs-target')
+          .removeAttr('data-action')
+          .removeAttr('id')
+          .attr("disabled", true);
+        $(this).click(function(event) {
+          event.preventDefault();
+          return;
+        });
+      });
+      $(this).find('button').each(function() {
+        $(this).attr("disabled", true);
+      });
+    } else if ($(this).hasClass('input-group')) {
+      $(this).find('input').each(function() {
+        $(this).removeClass('dropdown-toggle')
+          .removeAttr('data-bs-toggle')
+          .attr("disabled", true);
+        $(this).click(function(event) {
+          event.preventDefault();
+        });
+      });
+      $(this).find('button').each(function() {
+        $(this).attr("disabled", true);
+      });
+    } else if ($(this).hasClass('form-group')) {
+      $(this).find('input').each(function() {
+        $(this).attr("disabled", true);
+      });
+    } else if ($(this).hasClass('btn')) {
+      $(this).attr("disabled", true);
+    } else if ($(this).attr('data-provide') == 'slider') {
+      $(this).attr('disabled', true);
+    } else if ($(this).is(':checkbox')) {
+      $(this).attr("disabled", true);
+    }
+    $(this).data("toggle", "tooltip");
+    $(this).attr("title", lang_acl.prohibited);
+    $(this).tooltip();
+  });
+
+  // disable submit after submitting form (not API driven buttons)
+  $('form').submit(function() {
+    if ($('form button[type="submit"]').data('submitted') == '1') {
+      return false;
+    } else {
+      $(this).find('button[type="submit"]').first().text(lang_footer.loading);
+      $('form button[type="submit"]').attr('data-submitted', '1');
+      function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
+      $(document).on("keydown", disableF5);
+    }
+  });
+  // Textarea line numbers
+  $(".textarea-code").numberedtextarea({allowTabChar: true});
+  // trigger container restart
+  $('#RestartContainer').on('show.bs.modal', function(e) {
+    var container = $(e.relatedTarget).data('container');
+    $('#containerName').text(container);
+    $('#triggerRestartContainer').click(function(){
+      $(this).prop("disabled",true);
+      $(this).html('<div class="spinner-border text-white" role="status"><span class="visually-hidden">Loading...</span></div>');
+      $('#statusTriggerRestartContainer').html(lang_footer.restarting_container);
+      $.ajax({
+        method: 'get',
+        url: '/inc/ajax/container_ctrl.php',
+        timeout: docker_timeout,
+        data: {
+        'service': container,
+        'action': 'restart'
+        }
+      })
+      .always( function (data, status) {
+        $('#statusTriggerRestartContainer').append(data);
+        var htmlResponse = $.parseHTML(data)
+        if ($(htmlResponse).find('span').hasClass('text-success')) {
+          $('#triggerRestartContainer').html('<i class="bi bi-check-lg"></i> ');
+          setTimeout(function(){
+            $('#RestartContainer').modal('toggle');
+            window.location = window.location.href.split("#")[0];
+          }, 1200);
+        } else {
+          $('#triggerRestartContainer').html('<i class="bi bi-slash-lg"></i> ');
+        }
+      })
+    });
+  })
+
+  // Jquery Datatables, enable responsive plugin
+  $.extend($.fn.dataTable.defaults, {
+    responsive: true
+  });
+
+  // tag boxes
+  $('.tag-box .tag-add').click(function(){
+    addTag(this);
+  });
+  $(".tag-box .tag-input").keydown(function (e) {
+    if (e.which == 13){
+      e.preventDefault();
+      addTag(this);
+    }
+  });
+
+  // Dark Mode Loader
+  $('#dark-mode-toggle').click(toggleDarkMode);
+  if ($('#dark-mode-theme').length) {
+    $('#dark-mode-toggle').prop('checked', true);
+    if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
+    if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
+  }
+  function toggleDarkMode(){
+    if($('#dark-mode-theme').length){
+      $('#dark-mode-theme').remove();
+      $('#dark-mode-toggle').prop('checked', false);
+      if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
+      if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
+      localStorage.setItem('theme', 'light');
+    }else{
+      $('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
+      $('#dark-mode-toggle').prop('checked', true);
+      if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
+      if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
+      localStorage.setItem('theme', 'dark');
+    }
+  }
+});
+
+
+// https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+function escapeHtml(n){var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+function unescapeHtml(t){var n={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&#x2F;":"/","&#x60;":"`","&#x3D;":"="};return String(t).replace(/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F|&#x60|&#x3D;/g,function(t){return n[t]})}
+
+function addTag(tagAddElem, tag = null){
+  var tagboxElem = $(tagAddElem).parent();
+  var tagInputElem = $(tagboxElem).find(".tag-input")[0];
+  var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
+
+  if (!tag)
+    tag = $(tagInputElem).val();
+  if (!tag) return;
+  var value_tags = [];
+  try {
+    value_tags = JSON.parse($(tagValuesElem).val());
+  } catch {}
+  if (!Array.isArray(value_tags)) value_tags = [];
+  if (value_tags.includes(tag)) return;
+
+  $('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(tag) + '</span>').insertBefore('.tag-input').click(function(){
+    var del_tag = unescapeHtml($(this).text());
+    var del_tags = [];
+    try {
+      del_tags = JSON.parse($(tagValuesElem).val());
+    } catch {}
+    if (Array.isArray(del_tags)){
+      del_tags.splice(del_tags.indexOf(del_tag), 1);
+      $(tagValuesElem).val(JSON.stringify(del_tags));
+    }
+    $(this).remove();
+  });
+
+  value_tags.push(tag);
+  $(tagValuesElem).val(JSON.stringify(value_tags));
+  $(tagInputElem).val('');
+}

+ 1530 - 1533
data/web/js/site/debug.js

@@ -1,1533 +1,1530 @@
-$(document).ready(function() {
-  // Parse seconds ago to date
-  // Get "now" timestamp
-  var ts_now = Math.round((new Date()).getTime() / 1000);
-  $('.parse_s_ago').each(function(i, parse_s_ago) {
-    var started_s_ago = parseInt($(this).text(), 10);
-    if (typeof started_s_ago != 'NaN') {
-      var started_date = new Date((ts_now - started_s_ago) * 1000);
-      if (started_date instanceof Date && !isNaN(started_date)) {
-        var started_local_date = started_date.toLocaleDateString(undefined, {
-          year: "numeric",
-          month: "2-digit",
-          day: "2-digit",
-          hour: "2-digit",
-          minute: "2-digit",
-          second: "2-digit"
-        });
-        $(this).text(started_local_date);
-      } else {
-        $(this).text('-');
-      }
-    }
-  });
-  // Parse general dates
-  $('.parse_date').each(function(i, parse_date) {
-    var started_date = new Date(Date.parse($(this).text()));
-    if (typeof started_date != 'NaN') {
-      var started_local_date = started_date.toLocaleDateString(undefined, {
-        year: "numeric",
-        month: "2-digit",
-        day: "2-digit",
-        hour: "2-digit",
-        minute: "2-digit",
-        second: "2-digit"
-      });
-      $(this).text(started_local_date);
-    }
-  });
-
-  // set update loop container list
-  containersToUpdate = {}
-  // set default ChartJs Font Color
-  Chart.defaults.color = '#999';
-  // create host cpu and mem charts
-  createHostCpuAndMemChart();
-  // check for new version
-  if (mailcow_info.branch === "master"){
-    check_update(mailcow_info.version_tag, mailcow_info.project_url);
-  }
-  $("#maiclow_version").click(function(){
-    if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
-       mailcow_info.branch !== "master")
-      return;
-
-    showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
-  })
-  // get public ips
-  get_public_ips();
-  update_container_stats();
-});
-jQuery(function($){
-  if (localStorage.getItem("current_page") === null) {
-    var current_page = {};
-  } else {
-    var current_page = JSON.parse(localStorage.getItem('current_page'));
-  }
-  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
-  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
-  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
-  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
-  function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
-  function intToRGB(t){var n=(16777215&t).toString(16).toUpperCase();return"00000".substring(0,6-n.length)+n}
-  $(".refresh_table").on('click', function(e) {
-    e.preventDefault();
-    var table_name = $(this).data('table');
-    $('#' + table_name).DataTable().ajax.reload();
-  });
-  function draw_autodiscover_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#autodiscover_log') ) {
-      $('#autodiscover_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#autodiscover_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/autodiscover/100",
-        dataSrc: function(data){
-          return process_table_data(data, 'autodiscover_log');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          responsivePriority: 1,
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: 'User-Agent',
-          data: 'ua',
-          defaultContent: '',
-          className: 'dtr-col-md',
-          responsivePriority: 5
-        },
-        {
-          title: 'Username',
-          data: 'user',
-          defaultContent: '',
-          responsivePriority: 4
-        },
-        {
-          title: 'IP',
-          data: 'ip',
-          defaultContent: '',
-          responsivePriority: 2
-        },
-        {
-          title: 'Service',
-          data: 'service',
-          defaultContent: '',
-          responsivePriority: 3
-        }
-      ]
-    });
-  }
-  function draw_postfix_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#postfix_log') ) {
-      $('#postfix_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#postfix_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/postfix",
-        dataSrc: function(data){
-          return process_table_data(data, 'general_syslog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.priority,
-          data: 'priority',
-          defaultContent: ''
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: '',
-          className: 'dtr-col-md text-break'
-        }
-      ]
-    });
-  }
-  function draw_watchdog_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#watchdog_log') ) {
-      $('#watchdog_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#watchdog_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/watchdog",
-        dataSrc: function(data){
-          return process_table_data(data, 'watchdog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: 'Service',
-          data: 'service',
-          defaultContent: ''
-        },
-        {
-          title: 'Trend',
-          data: 'trend',
-          defaultContent: ''
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function draw_api_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#api_log') ) {
-      $('#api_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#api_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/api",
-        dataSrc: function(data){
-          return process_table_data(data, 'apilog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: 'URI',
-          data: 'uri',
-          defaultContent: '',
-          className: 'dtr-col-md dtr-break-all'
-        },
-        {
-          title: 'Method',
-          data: 'method',
-          defaultContent: ''
-        },
-        {
-          title: 'IP',
-          data: 'remote',
-          defaultContent: ''
-        },
-        {
-          title: 'Data',
-          data: 'data',
-          defaultContent: '',
-          className: 'dtr-col-md dtr-break-all'
-        }
-      ]
-    });
-  }
-  function draw_rl_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#rl_log') ) {
-      $('#rl_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#rl_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/ratelimited",
-        dataSrc: function(data){
-          return process_table_data(data, 'rllog');
-        }
-      },
-      columns: [
-        {
-          title: ' ',
-          data: 'indicator',
-          defaultContent: ''
-        },
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.rate_name,
-          data: 'rl_name',
-          defaultContent: ''
-        },
-        {
-          title: lang.sender,
-          data: 'from',
-          defaultContent: ''
-        },
-        {
-          title: lang.recipients,
-          data: 'rcpt',
-          defaultContent: ''
-        },
-        {
-          title: lang.authed_user,
-          data: 'user',
-          defaultContent: ''
-        },
-        {
-          title: 'Msg ID',
-          data: 'message_id',
-          defaultContent: ''
-        },
-        {
-          title: 'Header From',
-          data: 'header_from',
-          defaultContent: ''
-        },
-        {
-          title: 'Subject',
-          data: 'header_subject',
-          defaultContent: ''
-        },
-        {
-          title: 'Hash',
-          data: 'rl_hash',
-          defaultContent: ''
-        },
-        {
-          title: 'Rspamd QID',
-          data: 'qid',
-          defaultContent: ''
-        },
-        {
-          title: 'IP',
-          data: 'ip',
-          defaultContent: ''
-        },
-        {
-          title: lang.action,
-          data: 'action',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function draw_ui_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#ui_logs') ) {
-      $('#ui_logs').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#ui_logs').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/ui",
-        dataSrc: function(data){
-          return process_table_data(data, 'mailcow_ui');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: 'Type',
-          data: 'type',
-          defaultContent: ''
-        },
-        {
-          title: 'Task',
-          data: 'task',
-          defaultContent: ''
-        },
-        {
-          title: 'User',
-          data: 'user',
-          defaultContent: '',
-          className: 'dtr-col-sm'
-        },
-        {
-          title: 'Role',
-          data: 'role',
-          defaultContent: '',
-          className: 'dtr-col-sm'
-        },
-        {
-          title: 'IP',
-          data: 'remote',
-          defaultContent: '',
-          className: 'dtr-col-md dtr-break-all'
-        },
-        {
-          title: lang.message,
-          data: 'msg',
-          defaultContent: '',
-          className: 'dtr-col-md dtr-break-all'
-        },
-        {
-          title: 'Call',
-          data: 'call',
-          defaultContent: '',
-          className: 'none dtr-col-md dtr-break-all'
-        }
-      ]
-    });
-  }
-  function draw_sasl_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#sasl_logs') ) {
-      $('#sasl_logs').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#sasl_logs').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/sasl",
-        dataSrc: function(data){
-          return process_table_data(data, 'sasl_log_table');
-        }
-      },
-      columns: [
-        {
-          title: lang.username,
-          data: 'username',
-          defaultContent: ''
-        },
-        {
-          title: lang.service,
-          data: 'service',
-          defaultContent: ''
-        },
-        {
-          title: 'IP',
-          data: 'real_rip',
-          defaultContent: '',
-          className: 'dtr-col-md text-break'
-        },
-        {
-          title: lang.login_time,
-          data: 'datetime',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data.replace(/-/g, "/")); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        }
-      ]
-    });
-  }
-  function draw_acme_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#acme_log') ) {
-      $('#acme_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#acme_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/acme",
-        dataSrc: function(data){
-          return process_table_data(data, 'general_syslog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: '',
-          className: 'dtr-col-md dtr-break-all'
-        }
-      ]
-    });
-  }
-  function draw_netfilter_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#netfilter_log') ) {
-      $('#netfilter_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#netfilter_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/netfilter",
-        dataSrc: function(data){
-          return process_table_data(data, 'general_syslog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.priority,
-          data: 'priority',
-          defaultContent: ''
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: '',
-          className: 'dtr-col-md text-break'
-        }
-      ]
-    });
-  }
-  function draw_sogo_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#sogo_log') ) {
-      $('#sogo_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#sogo_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/sogo",
-        dataSrc: function(data){
-          return process_table_data(data, 'general_syslog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.priority,
-          data: 'priority',
-          defaultContent: ''
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: '',
-          className: 'dtr-col-md text-break'
-        }
-      ]
-    });
-  }
-  function draw_dovecot_logs() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#dovecot_log') ) {
-      $('#dovecot_log').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#dovecot_log').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      order: [[0, 'desc']],
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/dovecot",
-        dataSrc: function(data){
-          return process_table_data(data, 'general_syslog');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.priority,
-          data: 'priority',
-          defaultContent: ''
-        },
-        {
-          title: lang.message,
-          data: 'message',
-          defaultContent: '',
-          className: 'dtr-col-md text-break'
-        }
-      ]
-    });
-  }
-  function rspamd_pie_graph() {
-    $.ajax({
-      url: '/api/v1/get/rspamd/actions',
-      async: true,
-      success: function(data){
-        console.log(data);
-
-        var total = 0;
-        $(data).map(function(){total += this[1];});
-        var labels = $.makeArray($(data).map(function(){return this[0] + ' ' + Math.round(this[1]/total * 100) + '%';}));
-        var values = $.makeArray($(data).map(function(){return this[1];}));
-        console.log(values);
-
-        var graphdata = {
-          labels: labels,
-          datasets: [{
-            data: values,
-            backgroundColor: ['#DC3023', '#59ABE3', '#FFA400', '#FFA400', '#26A65B']
-          }]
-        };
-
-        var options = {
-          responsive: true,
-          maintainAspectRatio: false,
-          plugins: {
-            datalabels: {
-              color: '#FFF',
-              font: {
-                weight: 'bold'
-              },
-              display: function(context) {
-                return context.dataset.data[context.dataIndex] !== 0;
-              },
-              formatter: function(value, context) {
-                return Math.round(value/total*100) + '%';
-              }
-            }
-          }
-        };
-        var chartcanvas = document.getElementById('rspamd_donut');
-        Chart.register('ChartDataLabels');
-        if(typeof chart == 'undefined') {
-          chart = new Chart(chartcanvas.getContext("2d"), {
-            plugins: [ChartDataLabels],
-            type: 'doughnut',
-            data: graphdata,
-            options: options
-          });
-        }
-        else {
-          chart.destroy();
-          chart = new Chart(chartcanvas.getContext("2d"), {
-            plugins: [ChartDataLabels],
-            type: 'doughnut',
-            data: graphdata,
-            options: options
-          });
-        }
-      }
-    });
-  }
-  function draw_rspamd_history() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#rspamd_history') ) {
-      $('#rspamd_history').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#rspamd_history').DataTable({
-      processing: true,
-      serverSide: false,
-      language: lang_datatables,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/logs/rspamd-history",
-        dataSrc: function(data){
-          return process_table_data(data, 'rspamd_history');
-        }
-      },
-      columns: [
-        {
-          title: lang.time,
-          data: 'time',
-          defaultContent: '',
-          render: function(data, type){
-            var date = new Date(data ? data * 1000 : 0); 
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: 'IP address',
-          data: 'ip',
-          defaultContent: ''
-        },
-        {
-          title: 'From',
-          data: 'sender_mime',
-          defaultContent: ''
-        },
-        {
-          title: 'To',
-          data: 'rcpt',
-          defaultContent: ''
-        },
-        {
-          title: 'Subject',
-          data: 'subject',
-          defaultContent: ''
-        },
-        {
-          title: 'Action',
-          data: 'action',
-          defaultContent: ''
-        },
-        {
-          title: 'Score',
-          data: 'score',
-          defaultContent: ''
-        },
-        {
-          title: 'Subject',
-          data: 'header_subject',
-          defaultContent: ''
-        },
-        {
-          title: 'Symbols',
-          data: 'symbols',
-          defaultContent: '',
-          className: 'none dtr-col-md'
-        },
-        {
-          title: 'Msg size',
-          data: 'size',
-          defaultContent: ''
-        },
-        {
-          title: 'Scan Time',
-          data: 'scan_time',
-          defaultContent: ''
-        },
-        {
-          title: 'ID',
-          data: 'message-id',
-          defaultContent: ''
-        },
-        {
-          title: 'Authenticated user',
-          data: 'user',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function process_table_data(data, table) {
-    if (table == 'rspamd_history') {
-    $.each(data, function (i, item) {
-      if (item.rcpt_mime != "") {
-        item.rcpt = escapeHtml(item.rcpt_mime.join(", "));
-      }
-      else {
-        item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
-      }
-      item.symbols = Object.keys(item.symbols).sort(function (a, b) {
-        if (item.symbols[a].score === 0) return 1
-        if (item.symbols[b].score === 0) return -1
-        if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
-          return item.symbols[a].score - item.symbols[b].score
-        }
-        if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
-          return item.symbols[b].score - item.symbols[a].score
-        }
-        return item.symbols[b].score - item.symbols[a].score
-      }).map(function(key) {
-        var sym = item.symbols[key];
-        if (sym.score < 0) {
-          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
-        }
-        else if (sym.score === 0) {
-          sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
-        }
-        else {
-          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
-        }
-        var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
-        if (sym.options) {
-          str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
-        }
-        return str
-      }).join('<br>\n');
-      item.subject = escapeHtml(item.subject);
-      var scan_time = item.time_real.toFixed(3);
-      if (item.time_virtual) {
-        scan_time += ' / ' + item.time_virtual.toFixed(3);
-      }
-      item.scan_time = {
-        "options": {
-          "sortValue": item.time_real
-        },
-        "value": scan_time
-      };
-      if (item.action === 'clean' || item.action === 'no action') {
-        item.action = "<div class='badge fs-6 bg-success'>" + item.action + "</div>";
-      } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
-        item.action = "<div class='badge fs-6 bg-warning'>" + item.action + "</div>";
-      } else if (item.action === 'spam' || item.action === 'reject') {
-        item.action = "<div class='badge fs-6 bg-danger'>" + item.action + "</div>";
-      } else {
-        item.action = "<div class='badge fs-6 bg-info'>" + item.action + "</div>";
-      }
-      var score_content;
-      if (item.score < item.required_score) {
-        score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-      } else {
-        score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-      }
-      item.score = {
-        "options": {
-          "sortValue": item.score
-        },
-        "value": score_content
-      };
-      if (item.user == null) {
-        item.user = "none";
-      }
-    });
-    } else if (table == 'autodiscover_log') {
-      $.each(data, function (i, item) {
-        if (item.ua == null) {
-          item.ua = 'unknown';
-        } else {
-          item.ua = escapeHtml(item.ua);
-        }
-        item.ua = '<span style="font-size:small">' + item.ua + '</span>';
-        if (item.service == "activesync") {
-          item.service = '<span class="badge fs-6 bg-info">ActiveSync</span>';
-        }
-        else if (item.service == "imap") {
-          item.service = '<span class="badge fs-6 bg-success">IMAP, SMTP, Cal-/CardDAV</span>';
-        }
-        else {
-          item.service = '<span class="badge fs-6 bg-danger">' + escapeHtml(item.service) + '</span>';
-        }
-      });
-    } else if (table == 'watchdog') {
-      $.each(data, function (i, item) {
-        if (item.message == null) {
-          item.message = 'Health level: ' + item.lvl + '% (' + item.hpnow + '/' + item.hptotal + ')';
-          if (item.hpdiff < 0) {
-            item.trend = '<span class="badge fs-6 bg-danger"><i class="bi bi-caret-down-fill"></i> ' + item.hpdiff + '</span>';
-          }
-          else if (item.hpdiff == 0) {
-            item.trend = '<span class="badge fs-6 bg-info"><i class="bi bi-caret-right-fill"></i> ' + item.hpdiff + '</span>';
-          }
-          else {
-            item.trend = '<span class="badge fs-6 bg-success"><i class="bi bi-caret-up-fill"></i> ' + item.hpdiff + '</span>';
-          }
-        }
-        else {
-          item.trend = '';
-          item.service = '';
-        }
-      });
-    } else if (table == 'mailcow_ui') {
-      $.each(data, function (i, item) {
-        if (item === null) { return true; }
-        item.user = escapeHtml(item.user);
-        item.call = escapeHtml(item.call);
-        item.task = '<code>' + item.task + '</code>';
-        item.type = '<span class="badge fs-6 bg-' + item.type + '">' + item.type + '</span>';
-      });
-    } else if (table == 'sasl_log_table') {
-      $.each(data, function (i, item) {
-        if (item === null) { return true; }
-        item.username = escapeHtml(item.username);
-        item.service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
-      });
-    } else if (table == 'general_syslog') {
-      $.each(data, function (i, item) {
-        if (item === null) { return true; }
-        if (item.message.match("^base64,")) {
-          try {
-            item.message = atob(item.message.slice(7)).replace(/\\n/g, "<br />");
-          } catch(e) {
-            item.message = item.message.slice(7);
-          }
-        } else {
-          item.message = escapeHtml(item.message);
-        }
-        item.call = escapeHtml(item.call);
-        var danger_class = ["emerg", "alert", "crit", "err"];
-        var warning_class = ["warning", "warn"];
-        var info_class = ["notice", "info", "debug"];
-        if (jQuery.inArray(item.priority, danger_class) !== -1) {
-          item.priority = '<span class="badge fs-6 bg-danger">' + item.priority + '</span>';
-        } else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-          item.priority = '<span class="badge fs-6 bg-warning">' + item.priority + '</span>';
-        } else if (jQuery.inArray(item.priority, info_class) !== -1) {
-          item.priority = '<span class="badge fs-6 bg-info">' + item.priority + '</span>';
-        }
-      });
-    } else if (table == 'apilog') {
-      $.each(data, function (i, item) {
-        if (item === null) { return true; }
-        if (item.method == 'GET') {
-          item.method = '<span class="badge fs-6 bg-success">' + item.method + '</span>';
-        } else if (item.method == 'POST') {
-          item.method = '<span class="badge fs-6 bg-warning">' + item.method + '</span>';
-        }
-        item.data = escapeHtml(item.data);
-      });
-    } else if (table == 'rllog') {
-      $.each(data, function (i, item) {
-        if (item.user == null) {
-          item.user = "none";
-        }
-        if (item.rl_hash == null) {
-          item.rl_hash = "err";
-        }
-        item.indicator = '<span style="border-right:6px solid #' + intToRGB(hashCode(item.rl_hash)) + ';padding-left:5px;">&nbsp;</span>';
-        if (item.rl_hash != 'err') {
-          item.action = '<a href="#" data-action="delete_selected" data-id="single-hash" data-api-url="delete/rlhash" data-item="' + encodeURI(item.rl_hash) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.reset_limit + '</a>';
-        }
-      });
-    }
-    return data
-  };
-  $('.add_log_lines').on('click', function (e) {
-    e.preventDefault();
-    var log_table= $(this).data("table")
-    var new_nrows = $(this).data("nrows")
-    var post_process = $(this).data("post-process")
-    var log_url = $(this).data("log-url")
-    if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
-      console.log("no data-table or data-nrows or log_url or data-post-process attr found");
-      return;
-    }
-
-    if (table = $('#' + log_table).DataTable()) {
-      var heading = $('#' + log_table).closest('.card').find('.card-header');
-      var load_rows = (table.page.len() + 1) + '-' + (table.page.len() + new_nrows)
-
-      $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
-        if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
-        var rows = process_table_data(data, post_process);
-        var rows_now = (table.page.len() + data.length);
-        $(heading).children('.table-lines').text(rows_now)
-        mailcow_alert_box(data.length + lang.additional_rows, "success");
-        table.rows.add(rows).draw();
-      });
-    }
-  })
-
-  // detect element visibility changes
-  function onVisible(element, callback) {
-    $(document).ready(function() {
-      element_object = document.querySelector(element);
-      if (element_object === null) return;
-
-      new IntersectionObserver((entries, observer) => {
-        entries.forEach(entry => {
-          if(entry.intersectionRatio > 0) {
-            callback(element_object);
-          }
-        });
-      }).observe(element_object);
-    });
-  }
-  // Draw Table if tab is active
-  onVisible("[id^=postfix_log]", () => draw_postfix_logs());
-  onVisible("[id^=dovecot_log]", () => draw_dovecot_logs());
-  onVisible("[id^=sogo_log]", () => draw_sogo_logs());
-  onVisible("[id^=watchdog_log]", () => draw_watchdog_logs());
-  onVisible("[id^=autodiscover_log]", () => draw_autodiscover_logs());
-  onVisible("[id^=acme_log]", () => draw_acme_logs());
-  onVisible("[id^=api_log]", () => draw_api_logs());
-  onVisible("[id^=rl_log]", () => draw_rl_logs());
-  onVisible("[id^=ui_logs]", () => draw_ui_logs());
-  onVisible("[id^=sasl_logs]", () => draw_sasl_logs());
-  onVisible("[id^=netfilter_log]", () => draw_netfilter_logs());
-  onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
-  onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
-
-
-
-  // start polling host stats if tab is active
-  onVisible("[id^=tab-containers]", () => update_stats());
-  // start polling container stats if collapse is active
-  var containerElements = document.querySelectorAll(".container-details-collapse");
-  for (let i = 0; i < containerElements.length; i++){
-    new IntersectionObserver((entries, observer) => {
-      entries.forEach(entry => {
-        if(entry.intersectionRatio > 0) {
-
-          if (!containerElements[i].classList.contains("show")){
-            var container = containerElements[i].id.replace("Collapse", "");
-            var container_id = containerElements[i].getAttribute("data-id");
-
-            // check if chart exists or needs to be created
-            if (!Chart.getChart(container + "_DiskIOChart"))
-              createReadWriteChart(container + "_DiskIOChart", "Read", "Write");
-            if (!Chart.getChart(container + "_NetIOChart"))
-              createReadWriteChart(container + "_NetIOChart", "Recv", "Sent");
-
-            // add container to polling list
-            containersToUpdate[container] = {
-              id: container_id,
-              state: "idle"
-            }
-
-            // stop polling if collapse is closed
-            containerElements[i].addEventListener('hidden.bs.collapse', function () {
-              var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
-              var netIOCtx = Chart.getChart(container + "_NetIOChart");
-
-              diskIOCtx.data.datasets[0].data = [];
-              diskIOCtx.data.datasets[1].data = [];
-              diskIOCtx.data.labels = [];
-              netIOCtx.data.datasets[0].data = [];
-              netIOCtx.data.datasets[1].data = [];
-              netIOCtx.data.labels = [];
-            
-              diskIOCtx.update();
-              netIOCtx.update();
-              
-              delete containersToUpdate[container];
-            });
-          }
-
-        }
-      });
-    }).observe(containerElements[i]);
-  }
-});
-
-
-// update system stats - every 5 seconds if system & container tab is active
-function update_stats(timeout=5){
-  if (!$('#tab-containers').hasClass('active')) {
-    // tab not active - dont fetch stats - run again in n seconds
-    return;
-  }
-
-  window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) {
-    return response.json();
-  }).then(function(data) {
-    console.log(data);
-
-    if (data){
-      // display table 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() + "%");
-
-      // update cpu and mem chart
-      var cpu_chart = Chart.getChart("host_cpu_chart");
-      var mem_chart = Chart.getChart("host_mem_chart");
-
-      cpu_chart.data.labels.push(data.system_time.split(" ")[1]);
-      if (cpu_chart.data.labels.length > 30) cpu_chart.data.labels.shift();
-      mem_chart.data.labels.push(data.system_time.split(" ")[1]);
-      if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
-
-      cpu_chart.data.datasets[0].data.push(data.cpu.usage);
-      if (cpu_chart.data.datasets[0].data.length > 30)  cpu_chart.data.datasets[0].data.shift();
-      mem_chart.data.datasets[0].data.push(data.memory.usage);
-      if (mem_chart.data.datasets[0].data.length > 30)  mem_chart.data.datasets[0].data.shift();
-
-      cpu_chart.update();
-      mem_chart.update();
-    }
-
-    // run again in n seconds
-    setTimeout(update_stats, timeout * 1000);
-  });
-}
-// update specific container stats - every n (default 5s) seconds
-function update_container_stats(timeout=5){
-  
-  if ($('#tab-containers').hasClass('active')) {
-    for (let container in containersToUpdate){
-      container_id = containersToUpdate[container].id;
-      // check if container update stats is already running
-      if (containersToUpdate[container].state == "running")
-        continue;
-      containersToUpdate[container].state = "running";
-
-
-      window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) {
-        return response.json();
-      }).then(function(data) {
-        var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
-        var netIOCtx = Chart.getChart(container + "_NetIOChart");
-
-        console.log(container);
-        console.log(data);
-        prev_stats = null;
-        if (data.length >= 2){
-          prev_stats = data[data.length -2];
-
-          // hide spinners if we collected enough data
-          $('#' + container + "_DiskIOChart").removeClass('d-none');
-          $('#' + container + "_DiskIOChart").prev().addClass('d-none');
-          $('#' + container + "_NetIOChart").removeClass('d-none');
-          $('#' + container + "_NetIOChart").prev().addClass('d-none');
-        }
-          
-        data = data[data.length -1];
-
-        if (prev_stats != null){
-          // calc time diff
-          var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000;
-    
-          // calc disk io b/s
-          if ('io_service_bytes_recursive' in prev_stats.blkio_stats && prev_stats.blkio_stats.io_service_bytes_recursive !== null){
-            var prev_read_bytes = 0;
-            var prev_write_bytes = 0;
-            for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){
-              if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read")
-                prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
-              else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write")
-                prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
-            }
-            var read_bytes = 0;
-            var write_bytes = 0;
-            for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){
-              if (data.blkio_stats.io_service_bytes_recursive[i].op == "read")
-                read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
-              else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write")
-                write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
-            }
-            var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff;
-            var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff;
-          }
-    
-          // calc net io b/s
-          if ('networks' in prev_stats){
-            var prev_recv_bytes = 0;
-            var prev_sent_bytes = 0;
-            for (var key in prev_stats.networks){
-              prev_recv_bytes += prev_stats.networks[key].rx_bytes;
-              prev_sent_bytes += prev_stats.networks[key].tx_bytes;
-            }
-            var recv_bytes = 0;
-            var sent_bytes = 0;
-            for (var key in data.networks){
-              recv_bytes += data.networks[key].rx_bytes;
-              sent_bytes += data.networks[key].tx_bytes;
-            }
-            var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff;
-            var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff;
-          }
-    
-          addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, "");
-          addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, "");
-        }
-    
-        // run again in n seconds
-        containersToUpdate[container].state = "idle";
-      }).catch(err => {
-        console.log(err);
-      });
-    }
-  }
-
-  // run again in n seconds
-  setTimeout(update_container_stats, timeout * 1000);
-}
-// get public ips
-function get_public_ips(){
-  window.fetch("/api/v1/get/status/host/ip", {method:'GET',cache:'no-cache'}).then(function(response) {
-    return response.json();
-  }).then(function(data) {
-    console.log(data);
-
-    // display host ips
-    if (data.ipv4)
-      $("#host_ipv4").text(data.ipv4);
-    if (data.ipv6)
-      $("#host_ipv6").text(data.ipv6);
-  });
-}
-// 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;
-} 
-// format bytes to readable string
-function formatBytes(bytes){
-  // b
-  if (bytes < 1000) return bytes.toFixed(2).toString()+' B/s';
-  // b to kb
-  bytes = bytes / 1024;
-  if (bytes < 1000) return bytes.toFixed(2).toString()+' KB/s';
-  // kb to mb
-  bytes = bytes / 1024;
-  if (bytes < 1000) return bytes.toFixed(2).toString()+' MB/s';
-  // final mb to gb
-  return (bytes / 1024).toFixed(2).toString()+' GB/s';
-}
-// create read write line chart
-function createReadWriteChart(chart_id, read_lable, write_lable){
-  var ctx = document.getElementById(chart_id);
-
-  var dataNet = {
-    labels: [],
-    datasets: [{
-      label: read_lable,
-      backgroundColor: "rgba(41, 187, 239, 0.3)",
-      borderColor: "rgba(41, 187, 239, 0.6)",
-      pointRadius: 1,
-      pointHitRadius: 6,
-      borderWidth: 2,
-      fill: true,
-      tension: 0.2,
-      data: []
-    }, {
-      label: write_lable,
-      backgroundColor: "rgba(239, 60, 41, 0.3)",
-      borderColor: "rgba(239, 60, 41, 0.6)",
-      pointRadius: 1,
-      pointHitRadius: 6,
-      borderWidth: 2,
-      fill: true,
-      tension: 0.2,
-      data: []
-    }]
-  };
-  var optionsNet = {
-    interaction: {
-        mode: 'index'
-    },
-    scales: {
-      yAxis: {
-        min: 0,
-        grid: {
-            display: false
-        },
-        ticks: {
-          callback: function(i, index, ticks) {
-             return formatBytes(i);
-          }
-        }  
-      },
-      xAxis: {
-        grid: {
-            display: false
-        }  
-      }
-    }
-  };
-  
-  return new Chart(ctx, {
-    type: 'line',
-    data: dataNet,
-    options: optionsNet
-  });
-}
-// add to read write line chart
-function addReadWriteChart(chart_context, read_point, write_point, time, limit = 30){
-  // push time label for x-axis
-  chart_context.data.labels.push(time);
-  if (chart_context.data.labels.length > limit) chart_context.data.labels.shift();
-
-  // push datapoints
-  chart_context.data.datasets[0].data.push(read_point);
-  chart_context.data.datasets[1].data.push(write_point);
-  // shift data if more than 20 entires exists
-  if (chart_context.data.datasets[0].data.length > limit)  chart_context.data.datasets[0].data.shift();
-  if (chart_context.data.datasets[1].data.length > limit) chart_context.data.datasets[1].data.shift();
-
-  chart_context.update();
-}
-// create host cpu and mem chart
-function createHostCpuAndMemChart(){
-  var cpu_ctx = document.getElementById("host_cpu_chart");
-  var mem_ctx = document.getElementById("host_mem_chart");
-
-  var dataCpu = {
-    labels: [],
-    datasets: [{
-      label: "CPU %",
-      backgroundColor: "rgba(41, 187, 239, 0.3)",
-      borderColor: "rgba(41, 187, 239, 0.6)",
-      pointRadius: 1,
-      pointHitRadius: 6,
-      borderWidth: 2,
-      fill: true,
-      tension: 0.2,
-      data: []
-    }]
-  };
-  var optionsCpu = {
-    interaction: {
-        mode: 'index'
-    },
-    scales: {
-      yAxis: {
-        min: 0,
-        grid: {
-            display: false
-        },
-        ticks: {
-          callback: function(i, index, ticks) {
-             return i.toFixed(0).toString() + "%";
-          }
-        }  
-      },
-      xAxis: {
-        grid: {
-            display: false
-        }  
-      }
-    }
-  };
-
-  var dataMem = {
-    labels: [],
-    datasets: [{
-      label: "MEM %",
-      backgroundColor: "rgba(41, 187, 239, 0.3)",
-      borderColor: "rgba(41, 187, 239, 0.6)",
-      pointRadius: 1,
-      pointHitRadius: 6,
-      borderWidth: 2,
-      fill: true,
-      tension: 0.2,
-      data: []
-    }]
-  };
-  var optionsMem = {
-    interaction: {
-        mode: 'index'
-    },
-    scales: {
-      yAxis: {
-        min: 0,
-        grid: {
-            display: false
-        },
-        ticks: {
-          callback: function(i, index, ticks) {
-            return i.toFixed(0).toString() + "%";
-          }
-        }  
-      },
-      xAxis: {
-        grid: {
-            display: false
-        }  
-      }
-    }
-  };
-
-  
-  var net_io_chart = new Chart(cpu_ctx, {
-    type: 'line',
-    data: dataCpu,
-    options: optionsCpu
-  });
-  var disk_io_chart = new Chart(mem_ctx, {
-    type: 'line',
-    data: dataMem,
-    options: optionsMem
-  });
-}
-// check for mailcow updates
-function check_update(current_version, github_repo_url){
-  if (!current_version || !github_repo_url) return false; 
-
-  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(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
-        $("#mailcow_update_changelog").click(function(){
-          if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
-            return;
-      
-          showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
-        })
-      }
-    }).catch(err => {
-      // err
-      console.log(err);
-      $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
-      $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
-    });
-  }).catch(err => {
-    // err
-    console.log(err);
-    $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
-    $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
-  });
-}
-// show version changelog modal
-function showVersionModal(title, version){
-  $.ajax({
-    type: 'GET',
-    url: 'https://api.github.com/repos/' + mailcow_info.project_owner + '/' + mailcow_info.project_repo + '/releases/tags/' + version,
-    dataType: 'json',
-    success: function (data) { 
-      var md = window.markdownit();
-      var result = md.render(data.body);
-      result = parseGithubMarkdownLinks(result);
-
-      $('#showVersionModal').find(".modal-title").html(title);
-      $('#showVersionModal').find(".modal-body").html(`
-        <h3>` + data.name + `</h3>
-        <span class="mt-4">` + result + `</span>
-        <span><b>Github Link:</b> 
-          <a target="_blank" href="https://github.com/` + mailcow_info.project_owner + `/` + mailcow_info.project_repo + `/releases/tag/` + version + `">` + version + `</a>
-        </span>
-      `);
-
-      new bootstrap.Modal(document.getElementById("showVersionModal"), {
-        backdrop: 'static',
-        keyboard: false
-      }).show();
-    }
-  });
-}
-function parseGithubMarkdownLinks(inputText) {
-  var replacedText, replacePattern1;
-
-  replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
-  replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
-      if (matched.includes('github.com')){
-        // return short link if it's github link
-        last_uri_path = matched.split('/');
-        last_uri_path = last_uri_path[last_uri_path.length - 1];
-
-        // adjust Full Changelog link to match last git version and new git version, if link is a compare link
-        if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
-          matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
-          last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
-        }
-        
-        return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
-      };
-
-      // if it's not a github link, return complete link
-      return '<a href="' + matched + '" target="_blank">' + matched + '</a>'; 
-  });
-
-  return replacedText;
-}
+const LOCALE = undefined;
+const DATETIME_FORMAT = {
+  year: "numeric",
+  month: "2-digit",
+  day: "2-digit",
+  hour: "2-digit",
+  minute: "2-digit",
+  second: "2-digit"
+};
+
+$(document).ready(function() {
+  // Parse seconds ago to date
+  // Get "now" timestamp
+  var ts_now = Math.round((new Date()).getTime() / 1000);
+  $('.parse_s_ago').each(function(i, parse_s_ago) {
+    var started_s_ago = parseInt($(this).text(), 10);
+    if (typeof started_s_ago != 'NaN') {
+      var started_date = new Date((ts_now - started_s_ago) * 1000);
+      if (started_date instanceof Date && !isNaN(started_date)) {
+        var started_local_date = started_date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
+        $(this).text(started_local_date);
+      } else {
+        $(this).text('-');
+      }
+    }
+  });
+  // Parse general dates
+  $('.parse_date').each(function(i, parse_date) {
+    var started_date = new Date(Date.parse($(this).text()));
+    if (typeof started_date != 'NaN') {
+      var started_local_date = started_date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
+      $(this).text(started_local_date);
+    }
+  });
+
+  // set update loop container list
+  containersToUpdate = {}
+  // set default ChartJs Font Color
+  Chart.defaults.color = '#999';
+  // create host cpu and mem charts
+  createHostCpuAndMemChart();
+  // check for new version
+  if (mailcow_info.branch === "master"){
+    check_update(mailcow_info.version_tag, mailcow_info.project_url);
+  }
+  $("#maiclow_version").click(function(){
+    if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
+       mailcow_info.branch !== "master")
+      return;
+
+    showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
+  })
+  // get public ips
+  get_public_ips();
+  update_container_stats();
+});
+jQuery(function($){
+  if (localStorage.getItem("current_page") === null) {
+    var current_page = {};
+  } else {
+    var current_page = JSON.parse(localStorage.getItem('current_page'));
+  }
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
+  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+  function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
+  function intToRGB(t){var n=(16777215&t).toString(16).toUpperCase();return"00000".substring(0,6-n.length)+n}
+  $(".refresh_table").on('click', function(e) {
+    e.preventDefault();
+    var table_name = $(this).data('table');
+    $('#' + table_name).DataTable().ajax.reload();
+  });
+  function createSortableDate(td, cellData) {
+    $(td).attr({
+      "data-order": cellData,
+      "data-sort": cellData
+    });
+    $(td).html(convertTimestampToLocalFormat(cellData));
+  }
+  function draw_autodiscover_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#autodiscover_log') ) {
+      $('#autodiscover_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#autodiscover_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/autodiscover/100",
+        dataSrc: function(data){
+          return process_table_data(data, 'autodiscover_log');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          responsivePriority: 1,
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: 'User-Agent',
+          data: 'ua',
+          defaultContent: '',
+          className: 'dtr-col-md',
+          responsivePriority: 5
+        },
+        {
+          title: 'Username',
+          data: 'user',
+          defaultContent: '',
+          responsivePriority: 4
+        },
+        {
+          title: 'IP',
+          data: 'ip',
+          defaultContent: '',
+          responsivePriority: 2
+        },
+        {
+          title: 'Service',
+          data: 'service',
+          defaultContent: '',
+          responsivePriority: 3
+        }
+      ]
+    });
+  }
+  function draw_postfix_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#postfix_log') ) {
+      $('#postfix_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#postfix_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/postfix",
+        dataSrc: function(data){
+          return process_table_data(data, 'general_syslog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.priority,
+          data: 'priority',
+          defaultContent: ''
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: '',
+          className: 'dtr-col-md text-break'
+        }
+      ]
+    });
+  }
+  function draw_watchdog_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#watchdog_log') ) {
+      $('#watchdog_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#watchdog_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/watchdog",
+        dataSrc: function(data){
+          return process_table_data(data, 'watchdog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: 'Service',
+          data: 'service',
+          defaultContent: ''
+        },
+        {
+          title: 'Trend',
+          data: 'trend',
+          defaultContent: ''
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_api_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#api_log') ) {
+      $('#api_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#api_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/api",
+        dataSrc: function(data){
+          return process_table_data(data, 'apilog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: 'URI',
+          data: 'uri',
+          defaultContent: '',
+          className: 'dtr-col-md dtr-break-all'
+        },
+        {
+          title: 'Method',
+          data: 'method',
+          defaultContent: ''
+        },
+        {
+          title: 'IP',
+          data: 'remote',
+          defaultContent: ''
+        },
+        {
+          title: 'Data',
+          data: 'data',
+          defaultContent: '',
+          className: 'dtr-col-md dtr-break-all'
+        }
+      ]
+    });
+  }
+  function draw_rl_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#rl_log') ) {
+      $('#rl_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#rl_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/ratelimited",
+        dataSrc: function(data){
+          return process_table_data(data, 'rllog');
+        }
+      },
+      columns: [
+        {
+          title: ' ',
+          data: 'indicator',
+          defaultContent: ''
+        },
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.rate_name,
+          data: 'rl_name',
+          defaultContent: ''
+        },
+        {
+          title: lang.sender,
+          data: 'from',
+          defaultContent: ''
+        },
+        {
+          title: lang.recipients,
+          data: 'rcpt',
+          defaultContent: ''
+        },
+        {
+          title: lang.authed_user,
+          data: 'user',
+          defaultContent: ''
+        },
+        {
+          title: 'Msg ID',
+          data: 'message_id',
+          defaultContent: ''
+        },
+        {
+          title: 'Header From',
+          data: 'header_from',
+          defaultContent: ''
+        },
+        {
+          title: 'Subject',
+          data: 'header_subject',
+          defaultContent: ''
+        },
+        {
+          title: 'Hash',
+          data: 'rl_hash',
+          defaultContent: ''
+        },
+        {
+          title: 'Rspamd QID',
+          data: 'qid',
+          defaultContent: ''
+        },
+        {
+          title: 'IP',
+          data: 'ip',
+          defaultContent: ''
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_ui_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#ui_logs') ) {
+      $('#ui_logs').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#ui_logs').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/ui",
+        dataSrc: function(data){
+          return process_table_data(data, 'mailcow_ui');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: 'Type',
+          data: 'type',
+          defaultContent: ''
+        },
+        {
+          title: 'Task',
+          data: 'task',
+          defaultContent: ''
+        },
+        {
+          title: 'User',
+          data: 'user',
+          defaultContent: '',
+          className: 'dtr-col-sm'
+        },
+        {
+          title: 'Role',
+          data: 'role',
+          defaultContent: '',
+          className: 'dtr-col-sm'
+        },
+        {
+          title: 'IP',
+          data: 'remote',
+          defaultContent: '',
+          className: 'dtr-col-md dtr-break-all'
+        },
+        {
+          title: lang.message,
+          data: 'msg',
+          defaultContent: '',
+          className: 'dtr-col-md dtr-break-all'
+        },
+        {
+          title: 'Call',
+          data: 'call',
+          defaultContent: '',
+          className: 'none dtr-col-md dtr-break-all'
+        }
+      ]
+    });
+  }
+  function draw_sasl_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#sasl_logs') ) {
+      $('#sasl_logs').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#sasl_logs').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/sasl",
+        dataSrc: function(data){
+          return process_table_data(data, 'sasl_log_table');
+        }
+      },
+      columns: [
+        {
+          title: lang.username,
+          data: 'username',
+          defaultContent: ''
+        },
+        {
+          title: lang.service,
+          data: 'service',
+          defaultContent: ''
+        },
+        {
+          title: 'IP',
+          data: 'real_rip',
+          defaultContent: '',
+          className: 'dtr-col-md text-break'
+        },
+        {
+          title: lang.login_time,
+          data: 'datetime',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            cellData = Math.floor((new Date(data.replace(/-/g, "/"))).getTime() / 1000);
+            createSortableDate(td, cellData)
+          }
+        }
+      ]
+    });
+  }
+  function draw_acme_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#acme_log') ) {
+      $('#acme_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#acme_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/acme",
+        dataSrc: function(data){
+          return process_table_data(data, 'general_syslog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: '',
+          className: 'dtr-col-md dtr-break-all'
+        }
+      ]
+    });
+  }
+  function draw_netfilter_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#netfilter_log') ) {
+      $('#netfilter_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#netfilter_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/netfilter",
+        dataSrc: function(data){
+          return process_table_data(data, 'general_syslog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.priority,
+          data: 'priority',
+          defaultContent: ''
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: '',
+          className: 'dtr-col-md text-break'
+        }
+      ]
+    });
+  }
+  function draw_sogo_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#sogo_log') ) {
+      $('#sogo_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#sogo_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/sogo",
+        dataSrc: function(data){
+          return process_table_data(data, 'general_syslog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.priority,
+          data: 'priority',
+          defaultContent: ''
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: '',
+          className: 'dtr-col-md text-break'
+        }
+      ]
+    });
+  }
+  function draw_dovecot_logs() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#dovecot_log') ) {
+      $('#dovecot_log').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#dovecot_log').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      order: [[0, 'desc']],
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/dovecot",
+        dataSrc: function(data){
+          return process_table_data(data, 'general_syslog');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: lang.priority,
+          data: 'priority',
+          defaultContent: ''
+        },
+        {
+          title: lang.message,
+          data: 'message',
+          defaultContent: '',
+          className: 'dtr-col-md text-break'
+        }
+      ]
+    });
+  }
+  function rspamd_pie_graph() {
+    $.ajax({
+      url: '/api/v1/get/rspamd/actions',
+      async: true,
+      success: function(data){
+        console.log(data);
+
+        var total = 0;
+        $(data).map(function(){total += this[1];});
+        var labels = $.makeArray($(data).map(function(){return this[0] + ' ' + Math.round(this[1]/total * 100) + '%';}));
+        var values = $.makeArray($(data).map(function(){return this[1];}));
+        console.log(values);
+
+        var graphdata = {
+          labels: labels,
+          datasets: [{
+            data: values,
+            backgroundColor: ['#DC3023', '#59ABE3', '#FFA400', '#FFA400', '#26A65B']
+          }]
+        };
+
+        var options = {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: {
+            datalabels: {
+              color: '#FFF',
+              font: {
+                weight: 'bold'
+              },
+              display: function(context) {
+                return context.dataset.data[context.dataIndex] !== 0;
+              },
+              formatter: function(value, context) {
+                return Math.round(value/total*100) + '%';
+              }
+            }
+          }
+        };
+        var chartcanvas = document.getElementById('rspamd_donut');
+        Chart.register('ChartDataLabels');
+        if(typeof chart == 'undefined') {
+          chart = new Chart(chartcanvas.getContext("2d"), {
+            plugins: [ChartDataLabels],
+            type: 'doughnut',
+            data: graphdata,
+            options: options
+          });
+        }
+        else {
+          chart.destroy();
+          chart = new Chart(chartcanvas.getContext("2d"), {
+            plugins: [ChartDataLabels],
+            type: 'doughnut',
+            data: graphdata,
+            options: options
+          });
+        }
+      }
+    });
+  }
+  function draw_rspamd_history() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#rspamd_history') ) {
+      $('#rspamd_history').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#rspamd_history').DataTable({
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/logs/rspamd-history",
+        dataSrc: function(data){
+          return process_table_data(data, 'rspamd_history');
+        }
+      },
+      columns: [
+        {
+          title: lang.time,
+          data: 'time',
+          defaultContent: '',
+          createdCell: function(td, cellData) {
+            createSortableDate(td, cellData)
+          }
+        },
+        {
+          title: 'IP address',
+          data: 'ip',
+          defaultContent: ''
+        },
+        {
+          title: 'From',
+          data: 'sender_mime',
+          defaultContent: ''
+        },
+        {
+          title: 'To',
+          data: 'rcpt',
+          defaultContent: ''
+        },
+        {
+          title: 'Subject',
+          data: 'subject',
+          defaultContent: ''
+        },
+        {
+          title: 'Action',
+          data: 'action',
+          defaultContent: ''
+        },
+        {
+          title: 'Score',
+          data: 'score',
+          defaultContent: ''
+        },
+        {
+          title: 'Subject',
+          data: 'header_subject',
+          defaultContent: ''
+        },
+        {
+          title: 'Symbols',
+          data: 'symbols',
+          defaultContent: '',
+          className: 'none dtr-col-md'
+        },
+        {
+          title: 'Msg size',
+          data: 'size',
+          defaultContent: ''
+        },
+        {
+          title: 'Scan Time',
+          data: 'scan_time',
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'message-id',
+          defaultContent: ''
+        },
+        {
+          title: 'Authenticated user',
+          data: 'user',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function process_table_data(data, table) {
+    if (table == 'rspamd_history') {
+    $.each(data, function (i, item) {
+      if (item.rcpt_mime != "") {
+        item.rcpt = escapeHtml(item.rcpt_mime.join(", "));
+      }
+      else {
+        item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
+      }
+      item.symbols = Object.keys(item.symbols).sort(function (a, b) {
+        if (item.symbols[a].score === 0) return 1
+        if (item.symbols[b].score === 0) return -1
+        if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
+          return item.symbols[a].score - item.symbols[b].score
+        }
+        if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
+          return item.symbols[b].score - item.symbols[a].score
+        }
+        return item.symbols[b].score - item.symbols[a].score
+      }).map(function(key) {
+        var sym = item.symbols[key];
+        if (sym.score < 0) {
+          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
+        }
+        else if (sym.score === 0) {
+          sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
+        }
+        else {
+          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
+        }
+        var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
+        if (sym.options) {
+          str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
+        }
+        return str
+      }).join('<br>\n');
+      item.subject = escapeHtml(item.subject);
+      var scan_time = item.time_real.toFixed(3);
+      if (item.time_virtual) {
+        scan_time += ' / ' + item.time_virtual.toFixed(3);
+      }
+      item.scan_time = {
+        "options": {
+          "sortValue": item.time_real
+        },
+        "value": scan_time
+      };
+      if (item.action === 'clean' || item.action === 'no action') {
+        item.action = "<div class='badge fs-6 bg-success'>" + item.action + "</div>";
+      } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
+        item.action = "<div class='badge fs-6 bg-warning'>" + item.action + "</div>";
+      } else if (item.action === 'spam' || item.action === 'reject') {
+        item.action = "<div class='badge fs-6 bg-danger'>" + item.action + "</div>";
+      } else {
+        item.action = "<div class='badge fs-6 bg-info'>" + item.action + "</div>";
+      }
+      var score_content;
+      if (item.score < item.required_score) {
+        score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      } else {
+        score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      }
+      item.score = {
+        "options": {
+          "sortValue": item.score
+        },
+        "value": score_content
+      };
+      if (item.user == null) {
+        item.user = "none";
+      }
+    });
+    } else if (table == 'autodiscover_log') {
+      $.each(data, function (i, item) {
+        if (item.ua == null) {
+          item.ua = 'unknown';
+        } else {
+          item.ua = escapeHtml(item.ua);
+        }
+        item.ua = '<span style="font-size:small">' + item.ua + '</span>';
+        if (item.service == "activesync") {
+          item.service = '<span class="badge fs-6 bg-info">ActiveSync</span>';
+        }
+        else if (item.service == "imap") {
+          item.service = '<span class="badge fs-6 bg-success">IMAP, SMTP, Cal-/CardDAV</span>';
+        }
+        else {
+          item.service = '<span class="badge fs-6 bg-danger">' + escapeHtml(item.service) + '</span>';
+        }
+      });
+    } else if (table == 'watchdog') {
+      $.each(data, function (i, item) {
+        if (item.message == null) {
+          item.message = 'Health level: ' + item.lvl + '% (' + item.hpnow + '/' + item.hptotal + ')';
+          if (item.hpdiff < 0) {
+            item.trend = '<span class="badge fs-6 bg-danger"><i class="bi bi-caret-down-fill"></i> ' + item.hpdiff + '</span>';
+          }
+          else if (item.hpdiff == 0) {
+            item.trend = '<span class="badge fs-6 bg-info"><i class="bi bi-caret-right-fill"></i> ' + item.hpdiff + '</span>';
+          }
+          else {
+            item.trend = '<span class="badge fs-6 bg-success"><i class="bi bi-caret-up-fill"></i> ' + item.hpdiff + '</span>';
+          }
+        }
+        else {
+          item.trend = '';
+          item.service = '';
+        }
+      });
+    } else if (table == 'mailcow_ui') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        item.user = escapeHtml(item.user);
+        item.call = escapeHtml(item.call);
+        item.task = '<code>' + item.task + '</code>';
+        item.type = '<span class="badge fs-6 bg-' + item.type + '">' + item.type + '</span>';
+      });
+    } else if (table == 'sasl_log_table') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        item.username = escapeHtml(item.username);
+        item.service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
+      });
+    } else if (table == 'general_syslog') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        if (item.message.match("^base64,")) {
+          try {
+            item.message = atob(item.message.slice(7)).replace(/\\n/g, "<br />");
+          } catch(e) {
+            item.message = item.message.slice(7);
+          }
+        } else {
+          item.message = escapeHtml(item.message);
+        }
+        item.call = escapeHtml(item.call);
+        var danger_class = ["emerg", "alert", "crit", "err"];
+        var warning_class = ["warning", "warn"];
+        var info_class = ["notice", "info", "debug"];
+        if (jQuery.inArray(item.priority, danger_class) !== -1) {
+          item.priority = '<span class="badge fs-6 bg-danger">' + item.priority + '</span>';
+        } else if (jQuery.inArray(item.priority, warning_class) !== -1) {
+          item.priority = '<span class="badge fs-6 bg-warning">' + item.priority + '</span>';
+        } else if (jQuery.inArray(item.priority, info_class) !== -1) {
+          item.priority = '<span class="badge fs-6 bg-info">' + item.priority + '</span>';
+        }
+      });
+    } else if (table == 'apilog') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        if (item.method == 'GET') {
+          item.method = '<span class="badge fs-6 bg-success">' + item.method + '</span>';
+        } else if (item.method == 'POST') {
+          item.method = '<span class="badge fs-6 bg-warning">' + item.method + '</span>';
+        }
+        item.data = escapeHtml(item.data);
+      });
+    } else if (table == 'rllog') {
+      $.each(data, function (i, item) {
+        if (item.user == null) {
+          item.user = "none";
+        }
+        if (item.rl_hash == null) {
+          item.rl_hash = "err";
+        }
+        item.indicator = '<span style="border-right:6px solid #' + intToRGB(hashCode(item.rl_hash)) + ';padding-left:5px;">&nbsp;</span>';
+        if (item.rl_hash != 'err') {
+          item.action = '<a href="#" data-action="delete_selected" data-id="single-hash" data-api-url="delete/rlhash" data-item="' + encodeURI(item.rl_hash) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.reset_limit + '</a>';
+        }
+      });
+    }
+    return data
+  };
+  $('.add_log_lines').on('click', function (e) {
+    e.preventDefault();
+    var log_table= $(this).data("table")
+    var new_nrows = $(this).data("nrows")
+    var post_process = $(this).data("post-process")
+    var log_url = $(this).data("log-url")
+    if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
+      console.log("no data-table or data-nrows or log_url or data-post-process attr found");
+      return;
+    }
+
+    if (table = $('#' + log_table).DataTable()) {
+      var heading = $('#' + log_table).closest('.card').find('.card-header');
+      var load_rows = (table.page.len() + 1) + '-' + (table.page.len() + new_nrows)
+
+      $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
+        if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
+        var rows = process_table_data(data, post_process);
+        var rows_now = (table.page.len() + data.length);
+        $(heading).children('.table-lines').text(rows_now)
+        mailcow_alert_box(data.length + lang.additional_rows, "success");
+        table.rows.add(rows).draw();
+      });
+    }
+  })
+
+  // detect element visibility changes
+  function onVisible(element, callback) {
+    $(document).ready(function() {
+      element_object = document.querySelector(element);
+      if (element_object === null) return;
+
+      new IntersectionObserver((entries, observer) => {
+        entries.forEach(entry => {
+          if(entry.intersectionRatio > 0) {
+            callback(element_object);
+          }
+        });
+      }).observe(element_object);
+    });
+  }
+  // Draw Table if tab is active
+  onVisible("[id^=postfix_log]", () => draw_postfix_logs());
+  onVisible("[id^=dovecot_log]", () => draw_dovecot_logs());
+  onVisible("[id^=sogo_log]", () => draw_sogo_logs());
+  onVisible("[id^=watchdog_log]", () => draw_watchdog_logs());
+  onVisible("[id^=autodiscover_log]", () => draw_autodiscover_logs());
+  onVisible("[id^=acme_log]", () => draw_acme_logs());
+  onVisible("[id^=api_log]", () => draw_api_logs());
+  onVisible("[id^=rl_log]", () => draw_rl_logs());
+  onVisible("[id^=ui_logs]", () => draw_ui_logs());
+  onVisible("[id^=sasl_logs]", () => draw_sasl_logs());
+  onVisible("[id^=netfilter_log]", () => draw_netfilter_logs());
+  onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
+  onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
+
+
+
+  // start polling host stats if tab is active
+  onVisible("[id^=tab-containers]", () => update_stats());
+  // start polling container stats if collapse is active
+  var containerElements = document.querySelectorAll(".container-details-collapse");
+  for (let i = 0; i < containerElements.length; i++){
+    new IntersectionObserver((entries, observer) => {
+      entries.forEach(entry => {
+        if(entry.intersectionRatio > 0) {
+
+          if (!containerElements[i].classList.contains("show")){
+            var container = containerElements[i].id.replace("Collapse", "");
+            var container_id = containerElements[i].getAttribute("data-id");
+
+            // check if chart exists or needs to be created
+            if (!Chart.getChart(container + "_DiskIOChart"))
+              createReadWriteChart(container + "_DiskIOChart", "Read", "Write");
+            if (!Chart.getChart(container + "_NetIOChart"))
+              createReadWriteChart(container + "_NetIOChart", "Recv", "Sent");
+
+            // add container to polling list
+            containersToUpdate[container] = {
+              id: container_id,
+              state: "idle"
+            }
+
+            // stop polling if collapse is closed
+            containerElements[i].addEventListener('hidden.bs.collapse', function () {
+              var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
+              var netIOCtx = Chart.getChart(container + "_NetIOChart");
+
+              diskIOCtx.data.datasets[0].data = [];
+              diskIOCtx.data.datasets[1].data = [];
+              diskIOCtx.data.labels = [];
+              netIOCtx.data.datasets[0].data = [];
+              netIOCtx.data.datasets[1].data = [];
+              netIOCtx.data.labels = [];
+
+              diskIOCtx.update();
+              netIOCtx.update();
+
+              delete containersToUpdate[container];
+            });
+          }
+
+        }
+      });
+    }).observe(containerElements[i]);
+  }
+});
+
+
+// update system stats - every 5 seconds if system & container tab is active
+function update_stats(timeout=5){
+  if (!$('#tab-containers').hasClass('active')) {
+    // tab not active - dont fetch stats - run again in n seconds
+    return;
+  }
+
+  window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) {
+    return response.json();
+  }).then(function(data) {
+    console.log(data);
+
+    if (data){
+      // display table 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() + "%");
+
+      // update cpu and mem chart
+      var cpu_chart = Chart.getChart("host_cpu_chart");
+      var mem_chart = Chart.getChart("host_mem_chart");
+
+      cpu_chart.data.labels.push(data.system_time.split(" ")[1]);
+      if (cpu_chart.data.labels.length > 30) cpu_chart.data.labels.shift();
+      mem_chart.data.labels.push(data.system_time.split(" ")[1]);
+      if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
+
+      cpu_chart.data.datasets[0].data.push(data.cpu.usage);
+      if (cpu_chart.data.datasets[0].data.length > 30)  cpu_chart.data.datasets[0].data.shift();
+      mem_chart.data.datasets[0].data.push(data.memory.usage);
+      if (mem_chart.data.datasets[0].data.length > 30)  mem_chart.data.datasets[0].data.shift();
+
+      cpu_chart.update();
+      mem_chart.update();
+    }
+
+    // run again in n seconds
+    setTimeout(update_stats, timeout * 1000);
+  });
+}
+// update specific container stats - every n (default 5s) seconds
+function update_container_stats(timeout=5){
+
+  if ($('#tab-containers').hasClass('active')) {
+    for (let container in containersToUpdate){
+      container_id = containersToUpdate[container].id;
+      // check if container update stats is already running
+      if (containersToUpdate[container].state == "running")
+        continue;
+      containersToUpdate[container].state = "running";
+
+
+      window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) {
+        return response.json();
+      }).then(function(data) {
+        var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
+        var netIOCtx = Chart.getChart(container + "_NetIOChart");
+
+        console.log(container);
+        console.log(data);
+        prev_stats = null;
+        if (data.length >= 2){
+          prev_stats = data[data.length -2];
+
+          // hide spinners if we collected enough data
+          $('#' + container + "_DiskIOChart").removeClass('d-none');
+          $('#' + container + "_DiskIOChart").prev().addClass('d-none');
+          $('#' + container + "_NetIOChart").removeClass('d-none');
+          $('#' + container + "_NetIOChart").prev().addClass('d-none');
+        }
+
+        data = data[data.length -1];
+
+        if (prev_stats != null){
+          // calc time diff
+          var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000;
+
+          // calc disk io b/s
+          if ('io_service_bytes_recursive' in prev_stats.blkio_stats && prev_stats.blkio_stats.io_service_bytes_recursive !== null){
+            var prev_read_bytes = 0;
+            var prev_write_bytes = 0;
+            for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){
+              if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read")
+                prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
+              else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write")
+                prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
+            }
+            var read_bytes = 0;
+            var write_bytes = 0;
+            for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){
+              if (data.blkio_stats.io_service_bytes_recursive[i].op == "read")
+                read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
+              else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write")
+                write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
+            }
+            var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff;
+            var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff;
+          }
+
+          // calc net io b/s
+          if ('networks' in prev_stats){
+            var prev_recv_bytes = 0;
+            var prev_sent_bytes = 0;
+            for (var key in prev_stats.networks){
+              prev_recv_bytes += prev_stats.networks[key].rx_bytes;
+              prev_sent_bytes += prev_stats.networks[key].tx_bytes;
+            }
+            var recv_bytes = 0;
+            var sent_bytes = 0;
+            for (var key in data.networks){
+              recv_bytes += data.networks[key].rx_bytes;
+              sent_bytes += data.networks[key].tx_bytes;
+            }
+            var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff;
+            var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff;
+          }
+
+          addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, "");
+          addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, "");
+        }
+
+        // run again in n seconds
+        containersToUpdate[container].state = "idle";
+      }).catch(err => {
+        console.log(err);
+      });
+    }
+  }
+
+  // run again in n seconds
+  setTimeout(update_container_stats, timeout * 1000);
+}
+// get public ips
+function get_public_ips(){
+  window.fetch("/api/v1/get/status/host/ip", {method:'GET',cache:'no-cache'}).then(function(response) {
+    return response.json();
+  }).then(function(data) {
+    console.log(data);
+
+    // display host ips
+    if (data.ipv4)
+      $("#host_ipv4").text(data.ipv4);
+    if (data.ipv6)
+      $("#host_ipv6").text(data.ipv6);
+  });
+}
+// 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;
+}
+// format bytes to readable string
+function formatBytes(bytes){
+  // b
+  if (bytes < 1000) return bytes.toFixed(2).toString()+' B/s';
+  // b to kb
+  bytes = bytes / 1024;
+  if (bytes < 1000) return bytes.toFixed(2).toString()+' KB/s';
+  // kb to mb
+  bytes = bytes / 1024;
+  if (bytes < 1000) return bytes.toFixed(2).toString()+' MB/s';
+  // final mb to gb
+  return (bytes / 1024).toFixed(2).toString()+' GB/s';
+}
+// create read write line chart
+function createReadWriteChart(chart_id, read_lable, write_lable){
+  var ctx = document.getElementById(chart_id);
+
+  var dataNet = {
+    labels: [],
+    datasets: [{
+      label: read_lable,
+      backgroundColor: "rgba(41, 187, 239, 0.3)",
+      borderColor: "rgba(41, 187, 239, 0.6)",
+      pointRadius: 1,
+      pointHitRadius: 6,
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }, {
+      label: write_lable,
+      backgroundColor: "rgba(239, 60, 41, 0.3)",
+      borderColor: "rgba(239, 60, 41, 0.6)",
+      pointRadius: 1,
+      pointHitRadius: 6,
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }]
+  };
+  var optionsNet = {
+    interaction: {
+        mode: 'index'
+    },
+    scales: {
+      yAxis: {
+        min: 0,
+        grid: {
+            display: false
+        },
+        ticks: {
+          callback: function(i, index, ticks) {
+             return formatBytes(i);
+          }
+        }
+      },
+      xAxis: {
+        grid: {
+            display: false
+        }
+      }
+    }
+  };
+
+  return new Chart(ctx, {
+    type: 'line',
+    data: dataNet,
+    options: optionsNet
+  });
+}
+// add to read write line chart
+function addReadWriteChart(chart_context, read_point, write_point, time, limit = 30){
+  // push time label for x-axis
+  chart_context.data.labels.push(time);
+  if (chart_context.data.labels.length > limit) chart_context.data.labels.shift();
+
+  // push datapoints
+  chart_context.data.datasets[0].data.push(read_point);
+  chart_context.data.datasets[1].data.push(write_point);
+  // shift data if more than 20 entires exists
+  if (chart_context.data.datasets[0].data.length > limit)  chart_context.data.datasets[0].data.shift();
+  if (chart_context.data.datasets[1].data.length > limit) chart_context.data.datasets[1].data.shift();
+
+  chart_context.update();
+}
+// create host cpu and mem chart
+function createHostCpuAndMemChart(){
+  var cpu_ctx = document.getElementById("host_cpu_chart");
+  var mem_ctx = document.getElementById("host_mem_chart");
+
+  var dataCpu = {
+    labels: [],
+    datasets: [{
+      label: "CPU %",
+      backgroundColor: "rgba(41, 187, 239, 0.3)",
+      borderColor: "rgba(41, 187, 239, 0.6)",
+      pointRadius: 1,
+      pointHitRadius: 6,
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }]
+  };
+  var optionsCpu = {
+    interaction: {
+        mode: 'index'
+    },
+    scales: {
+      yAxis: {
+        min: 0,
+        grid: {
+            display: false
+        },
+        ticks: {
+          callback: function(i, index, ticks) {
+             return i.toFixed(0).toString() + "%";
+          }
+        }
+      },
+      xAxis: {
+        grid: {
+            display: false
+        }
+      }
+    }
+  };
+
+  var dataMem = {
+    labels: [],
+    datasets: [{
+      label: "MEM %",
+      backgroundColor: "rgba(41, 187, 239, 0.3)",
+      borderColor: "rgba(41, 187, 239, 0.6)",
+      pointRadius: 1,
+      pointHitRadius: 6,
+      borderWidth: 2,
+      fill: true,
+      tension: 0.2,
+      data: []
+    }]
+  };
+  var optionsMem = {
+    interaction: {
+        mode: 'index'
+    },
+    scales: {
+      yAxis: {
+        min: 0,
+        grid: {
+            display: false
+        },
+        ticks: {
+          callback: function(i, index, ticks) {
+            return i.toFixed(0).toString() + "%";
+          }
+        }
+      },
+      xAxis: {
+        grid: {
+            display: false
+        }
+      }
+    }
+  };
+
+
+  var net_io_chart = new Chart(cpu_ctx, {
+    type: 'line',
+    data: dataCpu,
+    options: optionsCpu
+  });
+  var disk_io_chart = new Chart(mem_ctx, {
+    type: 'line',
+    data: dataMem,
+    options: optionsMem
+  });
+}
+// check for mailcow updates
+function check_update(current_version, github_repo_url){
+  if (!current_version || !github_repo_url) return false;
+
+  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(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
+        $("#mailcow_update_changelog").click(function(){
+          if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
+            return;
+
+          showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
+        })
+      }
+    }).catch(err => {
+      // err
+      console.log(err);
+      $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
+      $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
+    });
+  }).catch(err => {
+    // err
+    console.log(err);
+    $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
+    $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
+  });
+}
+// show version changelog modal
+function showVersionModal(title, version){
+  $.ajax({
+    type: 'GET',
+    url: 'https://api.github.com/repos/' + mailcow_info.project_owner + '/' + mailcow_info.project_repo + '/releases/tags/' + version,
+    dataType: 'json',
+    success: function (data) {
+      var md = window.markdownit();
+      var result = md.render(data.body);
+      result = parseGithubMarkdownLinks(result);
+
+      $('#showVersionModal').find(".modal-title").html(title);
+      $('#showVersionModal').find(".modal-body").html(`
+        <h3>` + data.name + `</h3>
+        <span class="mt-4">` + result + `</span>
+        <span><b>Github Link:</b>
+          <a target="_blank" href="https://github.com/` + mailcow_info.project_owner + `/` + mailcow_info.project_repo + `/releases/tag/` + version + `">` + version + `</a>
+        </span>
+      `);
+
+      new bootstrap.Modal(document.getElementById("showVersionModal"), {
+        backdrop: 'static',
+        keyboard: false
+      }).show();
+    }
+  });
+}
+function parseGithubMarkdownLinks(inputText) {
+  var replacedText, replacePattern1;
+
+  replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
+  replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
+      if (matched.includes('github.com')){
+        // return short link if it's github link
+        last_uri_path = matched.split('/');
+        last_uri_path = last_uri_path[last_uri_path.length - 1];
+
+        // adjust Full Changelog link to match last git version and new git version, if link is a compare link
+        if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
+          matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
+          last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
+        }
+
+        return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
+      };
+
+      // if it's not a github link, return complete link
+      return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
+  });
+
+  return replacedText;
+}
+
+function convertTimestampToLocalFormat(timestamp) {
+  var date = new Date(timestamp ? timestamp * 1000 : 0);
+  return date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
+}

Some files were not shown because too many files changed in this diff