diff --git a/interface/lib/classes/finediff.inc.php b/interface/lib/classes/finediff.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..43b542f3629ec5ce14741267a0cdf991865bd193
--- /dev/null
+++ b/interface/lib/classes/finediff.inc.php
@@ -0,0 +1,752 @@
+<?php
+/**
+* FINE granularity DIFF
+*
+* Computes a set of instructions to convert the content of
+* one string into another.
+*
+* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
+*
+* Licensed under The MIT License
+* 
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*
+* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
+* @link http://www.raymondhill.net/finediff/
+* @version 0.6
+* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+*/
+
+/**
+* Usage (simplest):
+*
+*   include 'finediff.php';
+*
+*   // for the stock stack, granularity values are:
+*   // FineDiff::$paragraphGranularity = paragraph/line level
+*   // FineDiff::$sentenceGranularity = sentence level
+*   // FineDiff::$wordGranularity = word level
+*   // FineDiff::$characterGranularity = character level [default]
+*
+*   $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
+*   // store opcodes for later use...
+*
+*   ...
+*
+*   // restore $to_text from $from_text + $opcodes
+*   include 'finediff.php';
+*   $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
+*
+*   ...
+*/
+
+/**
+* Persisted opcodes (string) are a sequence of atomic opcode.
+* A single opcode can be one of the following:
+*   c | c{n} | d | d{n} | i:{c} | i{length}:{s}
+*   'c'        = copy one character from source
+*   'c{n}'     = copy n characters from source
+*   'd'        = skip one character from source
+*   'd{n}'     = skip n characters from source
+*   'i:{c}     = insert character 'c'
+*   'i{n}:{s}' = insert string s, which is of length n
+*
+* Do not exist as of now, under consideration:
+*   'm{n}:{o}  = move n characters from source o characters ahead.
+*   It would be essentially a shortcut for a delete->copy->insert
+*   command (swap) for when the inserted segment is exactly the same
+*   as the deleted one, and with only a copy operation in between.
+*   TODO: How often this case occurs? Is it worth it? Can only
+*   be done as a postprocessing method (->optimize()?)
+*/
+abstract class FineDiffOp {
+	abstract public function getFromLen();
+	abstract public function getToLen();
+	abstract public function getOpcode();
+	}
+
+class FineDiffDeleteOp extends FineDiffOp {
+	public function __construct($len) {
+		$this->fromLen = $len;
+		}
+	public function getFromLen() {
+		return $this->fromLen;
+		}
+	public function getToLen() {
+		return 0;
+		}
+	public function getOpcode() {
+		if ( $this->fromLen === 1 ) {
+			return 'd';
+			}
+		return "d{$this->fromLen}";
+		}
+	}
+
+class FineDiffInsertOp extends FineDiffOp {
+	public function __construct($text) {
+		$this->text = $text;
+		}
+	public function getFromLen() {
+		return 0;
+		}
+	public function getToLen() {
+		return strlen($this->text);
+		}
+	public function getText() {
+		return $this->text;
+		}
+	public function getOpcode() {
+		$to_len = strlen($this->text);
+		if ( $to_len === 1 ) {
+			return "i:{$this->text}";
+			}
+		return "i{$to_len}:{$this->text}";
+		}
+	}
+
+class FineDiffReplaceOp extends FineDiffOp {
+	public function __construct($fromLen, $text) {
+		$this->fromLen = $fromLen;
+		$this->text = $text;
+		}
+	public function getFromLen() {
+		return $this->fromLen;
+		}
+	public function getToLen() {
+		return strlen($this->text);
+		}
+	public function getText() {
+		return $this->text;
+		}
+	public function getOpcode() {
+		if ( $this->fromLen === 1 ) {
+			$del_opcode = 'd';
+			}
+		else {
+			$del_opcode = "d{$this->fromLen}";
+			}
+		$to_len = strlen($this->text);
+		if ( $to_len === 1 ) {
+			return "{$del_opcode}i:{$this->text}";
+			}
+		return "{$del_opcode}i{$to_len}:{$this->text}";
+		}
+	}
+
+class FineDiffCopyOp extends FineDiffOp {
+	public function __construct($len) {
+		$this->len = $len;
+		}
+	public function getFromLen() {
+		return $this->len;
+		}
+	public function getToLen() {
+		return $this->len;
+		}
+	public function getOpcode() {
+		if ( $this->len === 1 ) {
+			return 'c';
+			}
+		return "c{$this->len}";
+		}
+	public function increase($size) {
+		return $this->len += $size;
+		}
+	}
+
+/**
+* FineDiff ops
+*
+* Collection of ops
+*/
+class FineDiffOps {
+	public function appendOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' ) {
+			$edits[] = new FineDiffCopyOp($from_len);
+			}
+		else if ( $opcode === 'd' ) {
+			$edits[] = new FineDiffDeleteOp($from_len);
+			}
+		else /* if ( $opcode === 'i' ) */ {
+			$edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
+			}
+		}
+	public $edits = array();
+	}
+
+/**
+* FineDiff class
+*
+* TODO: Document
+*
+*/
+class FineDiff {
+
+	/**------------------------------------------------------------------------
+	*
+	* Public section
+	*
+	*/
+
+	/**
+	* Constructor
+	* ...
+	* The $granularityStack allows FineDiff to be configurable so that
+	* a particular stack tailored to the specific content of a document can
+	* be passed.
+	*/
+	public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
+		// setup stack for generic text documents by default
+		$this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
+		$this->edits = array();
+		$this->from_text = $from_text;
+		$this->doDiff($from_text, $to_text);
+		}
+
+	public function getOps() {
+		return $this->edits;
+		}
+
+	public function getOpcodes() {
+		$opcodes = array();
+		foreach ( $this->edits as $edit ) {
+			$opcodes[] = $edit->getOpcode();
+			}
+		return implode('', $opcodes);
+		}
+
+	public function renderDiffToHTML() {
+		$in_offset = 0;
+		ob_start();
+		foreach ( $this->edits as $edit ) {
+			$n = $edit->getFromLen();
+			if ( $edit instanceof FineDiffCopyOp ) {
+				FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+				}
+			else if ( $edit instanceof FineDiffDeleteOp ) {
+				FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+				}
+			else if ( $edit instanceof FineDiffInsertOp ) {
+				FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+				}
+			else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+				FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+				FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+				}
+			$in_offset += $n;
+			}
+		return ob_get_clean();
+		}
+
+	/**------------------------------------------------------------------------
+	* Return an opcodes string describing the diff between a "From" and a
+	* "To" string
+	*/
+	public static function getDiffOpcodes($from, $to, $granularities = null) {
+		$diff = new FineDiff($from, $to, $granularities);
+		return $diff->getOpcodes();
+		}
+
+	/**------------------------------------------------------------------------
+	* Return an iterable collection of diff ops from an opcodes string
+	*/
+	public static function getDiffOpsFromOpcodes($opcodes) {
+		$diffops = new FineDiffOps();
+		FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
+		return $diffops->edits;
+		}
+
+	/**------------------------------------------------------------------------
+	* Re-create the "To" string from the "From" string and an "Opcodes" string
+	*/
+	public static function renderToTextFromOpcodes($from, $opcodes) {
+		ob_start();
+		FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+		return ob_get_clean();
+		}
+
+	/**------------------------------------------------------------------------
+	* Render the diff to an HTML string -- UTF8 unsafe
+	*/
+	public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+		ob_start();
+		FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+		return ob_get_clean();
+		}
+
+	/**------------------------------------------------------------------------
+	* Render the diff to an HTML string -- UTF8 safe
+	*/
+	public static function renderUTF8DiffToHTMLFromOpcodes($from, $opcodes) {
+		ob_start();
+		FineDiff::renderUTF8FromOpcode($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+		return ob_get_clean();
+		}
+
+	/**------------------------------------------------------------------------
+	* Generic opcodes parser, user must supply callback for handling
+	* single opcode
+	*/
+	public static function renderFromOpcodes($from, $opcodes, $callback) {
+		if ( !is_callable($callback) ) {
+			return;
+			}
+		$opcodes_len = strlen($opcodes);
+		$from_offset = $opcodes_offset = 0;
+		while ( $opcodes_offset <  $opcodes_len ) {
+			$opcode = substr($opcodes, $opcodes_offset, 1);
+			$opcodes_offset++;
+			$n = intval(substr($opcodes, $opcodes_offset));
+			if ( $n ) {
+				$opcodes_offset += strlen(strval($n));
+				}
+			else {
+				$n = 1;
+				}
+			if ( $opcode === 'c' ) { // copy n characters from source
+				call_user_func($callback, 'c', $from, $from_offset, $n, '');
+				$from_offset += $n;
+				}
+			else if ( $opcode === 'd' ) { // delete n characters from source
+				call_user_func($callback, 'd', $from, $from_offset, $n, '');
+				$from_offset += $n;
+				}
+			else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+				call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+				$opcodes_offset += 1 + $n;
+				}
+			}
+		}
+
+	/**------------------------------------------------------------------------
+	* Generic opcodes parser, user must supply callback for handling
+	* single opcode
+	*/
+	private static function renderUTF8FromOpcode($from, $opcodes, $callback) {
+		if ( !is_callable($callback) ) {
+			return;
+			}
+        $from_len = strlen($from);
+		$opcodes_len = strlen($opcodes);
+		$from_offset = $opcodes_offset = 0;
+        $last_to_chars = '';
+		while ( $opcodes_offset <  $opcodes_len ) {
+			$opcode = substr($opcodes, $opcodes_offset, 1);
+			$opcodes_offset++;
+			$n = intval(substr($opcodes, $opcodes_offset));
+			if ( $n ) {
+				$opcodes_offset += strlen(strval($n));
+				}
+			else {
+				$n = 1;
+				}
+            if ( $opcode === 'c' || $opcode === 'd' ) {
+                $beg = $from_offset;
+                $end = $from_offset + $n;
+                while ( $beg > 0 && (ord($from[$beg]) & 0xC0) === 0x80 ) { $beg--; }
+                while ( $end < $from_len && (ord($from[$end]) & 0xC0) === 0x80 ) { $end++; }
+                if ( $opcode === 'c' ) { // copy n characters from source
+                    call_user_func($callback, 'c', $from, $beg, $end - $beg, '');
+                    $last_to_chars = substr($from, $beg, $end - $beg);
+                    }
+                else /* if ( $opcode === 'd' ) */ { // delete n characters from source
+                    call_user_func($callback, 'd', $from, $beg, $end - $beg, '');
+                    }
+                $from_offset += $n;
+                }
+			else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+				$opcodes_offset += 1;
+                if ( strlen($last_to_chars) > 0 && (ord($opcodes[$opcodes_offset]) & 0xC0) === 0x80 ) {
+                    $beg = strlen($last_to_chars) - 1;
+                    while ( $beg > 0 && (ord($last_to_chars[$beg]) & 0xC0) === 0x80 ) { $beg--; }
+                    $prefix = substr($last_to_chars, $beg);
+                } else {
+                    $prefix = '';
+                }
+                $end = $from_offset;
+                while ( $end < $from_len && (ord($from[$end]) & 0xC0) === 0x80 ) { $end++; }
+                $toInsert = $prefix . substr($opcodes, $opcodes_offset, $n) . substr($from, $end, $end - $from_offset);
+                call_user_func($callback, 'i', $toInsert, 0, strlen($toInsert));
+				$opcodes_offset += $n;
+                $last_to_chars = $toInsert;
+                }
+            }
+        }
+
+	/**
+	* Stock granularity stacks and delimiters
+	*/
+
+	const paragraphDelimiters = "\n\r";
+	public static $paragraphGranularity = array(
+		FineDiff::paragraphDelimiters
+		);
+	const sentenceDelimiters = ".\n\r";
+	public static $sentenceGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters
+		);
+	const wordDelimiters = " \t.\n\r";
+	public static $wordGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters,
+		FineDiff::wordDelimiters
+		);
+	const characterDelimiters = "";
+	public static $characterGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters,
+		FineDiff::wordDelimiters,
+		FineDiff::characterDelimiters
+		);
+
+	public static $textStack = array(
+		".",
+		" \t.\n\r",
+		""
+		);
+
+	/**------------------------------------------------------------------------
+	*
+	* Private section
+	*
+	*/
+
+	/**
+	* Entry point to compute the diff.
+	*/
+	private function doDiff($from_text, $to_text) {
+		$this->last_edit = false;
+		$this->stackpointer = 0;
+		$this->from_text = $from_text;
+		$this->from_offset = 0;
+		// can't diff without at least one granularity specifier
+		if ( empty($this->granularityStack) ) {
+			return;
+			}
+		$this->_processGranularity($from_text, $to_text);
+		}
+
+	/**
+	* This is the recursive function which is responsible for
+	* handling/increasing granularity.
+	*
+	* Incrementally increasing the granularity is key to compute the
+	* overall diff in a very efficient way.
+	*/
+	private function _processGranularity($from_segment, $to_segment) {
+		$delimiters = $this->granularityStack[$this->stackpointer++];
+		$has_next_stage = $this->stackpointer < count($this->granularityStack);
+		foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
+			// increase granularity
+			if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
+				$this->_processGranularity(
+					substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
+					$fragment_edit->getText()
+					);
+				}
+			// fuse copy ops whenever possible
+			else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
+				$this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
+				$this->from_offset += $fragment_edit->getFromLen();
+				}
+			else {
+				/* $fragment_edit instanceof FineDiffCopyOp */
+				/* $fragment_edit instanceof FineDiffDeleteOp */
+				/* $fragment_edit instanceof FineDiffInsertOp */
+				$this->edits[] = $this->last_edit = $fragment_edit;
+				$this->from_offset += $fragment_edit->getFromLen();
+				}
+			}
+		$this->stackpointer--;
+		}
+
+	/**
+	* This is the core algorithm which actually perform the diff itself,
+	* fragmenting the strings as per specified delimiters.
+	*
+	* This function is naturally recursive, however for performance purpose
+	* a local job queue is used instead of outright recursivity.
+	*/
+	private static function doFragmentDiff($from_text, $to_text, $delimiters) {
+		// Empty delimiter means character-level diffing.
+		// In such case, use code path optimized for character-level
+		// diffing.
+		if ( empty($delimiters) ) {
+			return FineDiff::doCharDiff($from_text, $to_text);
+			}
+
+		$result = array();
+
+		// fragment-level diffing
+		$from_text_len = strlen($from_text);
+		$to_text_len = strlen($to_text);
+		$from_fragments = FineDiff::extractFragments($from_text, $delimiters);
+		$to_fragments = FineDiff::extractFragments($to_text, $delimiters);
+
+		$jobs = array(array(0, $from_text_len, 0, $to_text_len));
+
+		$cached_array_keys = array();
+
+		while ( $job = array_pop($jobs) ) {
+
+			// get the segments which must be diff'ed
+			list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+
+			// catch easy cases first
+			$from_segment_length = $from_segment_end - $from_segment_start;
+			$to_segment_length = $to_segment_end - $to_segment_start;
+			if ( !$from_segment_length || !$to_segment_length ) {
+				if ( $from_segment_length ) {
+					$result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
+					}
+				else if ( $to_segment_length ) {
+					$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
+					}
+				continue;
+				}
+
+			// find longest copy operation for the current segments
+			$best_copy_length = 0;
+
+			$from_base_fragment_index = $from_segment_start;
+
+			$cached_array_keys_for_current_segment = array();
+
+			while ( $from_base_fragment_index < $from_segment_end ) {
+				$from_base_fragment = $from_fragments[$from_base_fragment_index];
+				$from_base_fragment_length = strlen($from_base_fragment);
+				// performance boost: cache array keys
+				if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
+					if ( !isset($cached_array_keys[$from_base_fragment]) ) {
+						$to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
+						}
+					else {
+						$to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
+						}
+					// get only indices which falls within current segment
+					if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
+						$to_fragment_indices = array();
+						foreach ( $to_all_fragment_indices as $to_fragment_index ) {
+							if ( $to_fragment_index < $to_segment_start ) { continue; }
+							if ( $to_fragment_index >= $to_segment_end ) { break; }
+							$to_fragment_indices[] = $to_fragment_index;
+							}
+						$cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
+						}
+					else {
+						$to_fragment_indices = $to_all_fragment_indices;
+						}
+					}
+				else {
+					$to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
+					}
+				// iterate through collected indices
+				foreach ( $to_fragment_indices as $to_base_fragment_index ) {
+					$fragment_index_offset = $from_base_fragment_length;
+					// iterate until no more match
+					for (;;) {
+						$fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
+						if ( $fragment_from_index >= $from_segment_end ) {
+							break;
+							}
+						$fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
+						if ( $fragment_to_index >= $to_segment_end ) {
+							break;
+							}
+						if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
+							break;
+							}
+						$fragment_length = strlen($from_fragments[$fragment_from_index]);
+						$fragment_index_offset += $fragment_length;
+						}
+					if ( $fragment_index_offset > $best_copy_length ) {
+						$best_copy_length = $fragment_index_offset;
+						$best_from_start = $from_base_fragment_index;
+						$best_to_start = $to_base_fragment_index;
+						}
+					}
+				$from_base_fragment_index += strlen($from_base_fragment);
+				// If match is larger than half segment size, no point trying to find better
+				// TODO: Really?
+				if ( $best_copy_length >= $from_segment_length / 2) {
+					break;
+					}
+				// no point to keep looking if what is left is less than
+				// current best match
+				if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
+					break;
+					}
+				}
+
+			if ( $best_copy_length ) {
+				$jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
+				$result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
+				$jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
+				}
+			else {
+				$result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
+				}
+			}
+
+		ksort($result, SORT_NUMERIC);
+		return array_values($result);
+		}
+
+	/**
+	* Perform a character-level diff.
+	*
+	* The algorithm is quite similar to doFragmentDiff(), except that
+	* the code path is optimized for character-level diff -- strpos() is
+	* used to find out the longest common subequence of characters.
+	*
+	* We try to find a match using the longest possible subsequence, which
+	* is at most the length of the shortest of the two strings, then incrementally
+	* reduce the size until a match is found.
+	*
+	* I still need to study more the performance of this function. It
+	* appears that for long strings, the generic doFragmentDiff() is more
+	* performant. For word-sized strings, doCharDiff() is somewhat more
+	* performant.
+	*/
+	private static function doCharDiff($from_text, $to_text) {
+		$result = array();
+		$jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
+		while ( $job = array_pop($jobs) ) {
+			// get the segments which must be diff'ed
+			list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+			$from_segment_len = $from_segment_end - $from_segment_start;
+			$to_segment_len = $to_segment_end - $to_segment_start;
+
+			// catch easy cases first
+			if ( !$from_segment_len || !$to_segment_len ) {
+				if ( $from_segment_len ) {
+					$result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
+					}
+				else if ( $to_segment_len ) {
+					$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
+					}
+				continue;
+				}
+			if ( $from_segment_len >= $to_segment_len ) {
+				$copy_len = $to_segment_len;
+				while ( $copy_len ) {
+					$to_copy_start = $to_segment_start;
+					$to_copy_start_max = $to_segment_end - $copy_len;
+					while ( $to_copy_start <= $to_copy_start_max ) {
+						$from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
+						if ( $from_copy_start !== false ) {
+							$from_copy_start += $from_segment_start;
+							break 2;
+							}
+						$to_copy_start++;
+						}
+					$copy_len--;
+					}
+				}
+			else {
+				$copy_len = $from_segment_len;
+				while ( $copy_len ) {
+					$from_copy_start = $from_segment_start;
+					$from_copy_start_max = $from_segment_end - $copy_len;
+					while ( $from_copy_start <= $from_copy_start_max ) {
+						$to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
+						if ( $to_copy_start !== false ) {
+							$to_copy_start += $to_segment_start;
+							break 2;
+							}
+						$from_copy_start++;
+						}
+					$copy_len--;
+					}
+				}
+			// match found
+			if ( $copy_len ) {
+				$jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
+				$result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
+				$jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
+				}
+			// no match,  so delete all, insert all
+			else {
+				$result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
+				}
+			}
+		ksort($result, SORT_NUMERIC);
+		return array_values($result);
+		}
+
+	/**
+	* Efficiently fragment the text into an array according to
+	* specified delimiters.
+	* No delimiters means fragment into single character.
+	* The array indices are the offset of the fragments into
+	* the input string.
+	* A sentinel empty fragment is always added at the end.
+	* Careful: No check is performed as to the validity of the
+	* delimiters.
+	*/
+	private static function extractFragments($text, $delimiters) {
+		// special case: split into characters
+		if ( empty($delimiters) ) {
+			$chars = str_split($text, 1);
+			$chars[strlen($text)] = '';
+			return $chars;
+			}
+		$fragments = array();
+		$start = $end = 0;
+		for (;;) {
+			$end += strcspn($text, $delimiters, $end);
+			$end += strspn($text, $delimiters, $end);
+			if ( $end === $start ) {
+				break;
+				}
+			$fragments[$start] = substr($text, $start, $end - $start);
+			$start = $end;
+			}
+		$fragments[$start] = '';
+		return $fragments;
+		}
+
+	/**
+	* Stock opcode renderers
+	*/
+	private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' || $opcode === 'i' ) {
+			echo substr($from, $from_offset, $from_len);
+			}
+		}
+
+	private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' ) {
+			echo htmlspecialchars(substr($from, $from_offset, $from_len));
+			}
+		else if ( $opcode === 'd' ) {
+			$deletion = substr($from, $from_offset, $from_len);
+			if ( strcspn($deletion, " \n\r") === 0 ) {
+				$deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+				}
+			echo '<del>', htmlspecialchars($deletion), '</del>';
+			}
+		else /* if ( $opcode === 'i' ) */ {
+ 			echo '<ins>', htmlspecialchars(substr($from, $from_offset, $from_len)), '</ins>';
+			}
+		}
+	}
+
diff --git a/interface/web/monitor/dataloghistory_list.php b/interface/web/monitor/dataloghistory_list.php
new file mode 100644
index 0000000000000000000000000000000000000000..cab8da8a3671454d32c6598bde1a74e5a1e512b5
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_list.php
@@ -0,0 +1,54 @@
+<?php
+
+/*
+Copyright (c) 2008, Till Brehm, projektfarm Gmbh
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once '../../lib/config.inc.php';
+require_once '../../lib/app.inc.php';
+
+/******************************************
+* Begin Form configuration
+******************************************/
+
+$list_def_file = "list/dataloghistory.list.php";
+
+/******************************************
+* End Form configuration
+******************************************/
+
+//* Check permissions for module
+$app->auth->check_module_permissions('monitor');
+
+$app->uses('listform_actions');
+
+$app->listform_actions->SQLOrderBy = "ORDER BY sys_datalog.tstamp DESC, sys_datalog.datalog_id DESC";
+
+$app->listform_actions->onLoad();
+
+
+?>
diff --git a/interface/web/monitor/dataloghistory_undo.php b/interface/web/monitor/dataloghistory_undo.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ff08faa0aa2a49fef1518c2262dc21861aad5d5
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_undo.php
@@ -0,0 +1,70 @@
+<?php
+
+/*
+Copyright (c) 2007-2008, Till Brehm, projektfarm Gmbh and Oliver Vogel www.muv.com
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once '../../lib/config.inc.php';
+require_once '../../lib/app.inc.php';
+
+//* Check permissions for module
+$app->auth->check_module_permissions('monitor');
+
+// Loading the template
+$app->uses('tpl');
+$app->tpl->newTemplate("form.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/dataloghistory_undo.htm');
+
+require('lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_undo.lng');
+$app->tpl->setvar($wb);
+
+$id = intval($_GET['id']);
+
+$record = $app->db->queryOneRecord('SELECT * FROM sys_datalog WHERE datalog_id = ?', $id);
+
+$dbidx = explode(':', $record['dbidx']);
+
+$old_record = $app->db->queryOneRecord('SELECT * FROM ?? WHERE ??=?', $record['dbtable'], $dbidx[0], $dbidx[1]);
+
+if (is_array($old_record)) {
+	if(!$data = unserialize(stripslashes($record['data']))) {
+		$data = unserialize($record['data']);
+	}
+
+	$new_record = $data['old'];
+
+	$app->db->datalogUpdate($record['dbtable'], $new_record, $dbidx[0], $dbidx[1]);
+
+	$app->tpl->setVar('success', true);
+} else {
+	$app->tpl->setVar('success', false);
+}
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+
+?>
diff --git a/interface/web/monitor/dataloghistory_view.php b/interface/web/monitor/dataloghistory_view.php
new file mode 100644
index 0000000000000000000000000000000000000000..e3b5e7b63c24b89bfc7bf75b6607c4d549f545cb
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_view.php
@@ -0,0 +1,130 @@
+<?php
+
+/*
+Copyright (c) 2007-2008, Till Brehm, projektfarm Gmbh and Oliver Vogel www.muv.com
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once '../../lib/config.inc.php';
+require_once '../../lib/app.inc.php';
+
+//* Check permissions for module
+$app->auth->check_module_permissions('monitor');
+
+$app->load('finediff');
+
+// Loading the template
+$app->uses('tpl');
+$app->tpl->newTemplate("form.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/dataloghistory_view.htm');
+
+$app->load_language_file('web/monitor/lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_view.lng');
+require('lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_view.lng');
+$app->tpl->setvar($wb);
+
+$id = intval($_GET['id']);
+
+$record = $app->db->queryOneRecord('SELECT * FROM sys_datalog WHERE datalog_id = ?', $id);
+
+$out['id'] = $id;
+
+$out['timestamp'] = date($app->lng('conf_format_datetime'), $record['tstamp']);
+$out['table'] = $record['dbtable'];
+
+$out['action_char'] = $record['action'];
+$out['action_name'] = $app->lng($record['action']);
+
+$out['session_id'] = $record['session_id'];
+
+if(!$data = unserialize(stripslashes($record['data']))) {
+	$data = unserialize($record['data']);
+}
+
+switch ($record['action']) {
+	case 'i':
+		$inserts = array();
+		foreach ($data['new'] as $key=>$value) {
+			$inserts[] = array(
+				'key' => $key,
+				'value' => nl2br($value),
+			);
+		}
+		$app->tpl->setLoop('inserts', $inserts);
+		break;
+	case 'u':
+		$updates = array();
+		foreach ($data['new'] as $key=>$value) {
+			if ($value != $data['old'][$key]) {
+				$old = $data['old'][$key];
+				$new = $value;
+				$changes = show_diff_if_needed($old, $new);
+				$updates[] = array(
+					'key' => $key,
+					'is_diff' => $changes['is_diff'],
+					'old' => nl2br($changes['old']),
+					'new' => nl2br($changes['new']),
+					'diff' => nl2br($changes['diff']),
+				);
+			}
+		}
+		if (count($updates) > 0) {
+			$app->tpl->setLoop('updates', $updates);
+		} else {
+			$out['no_changes'] = true;
+		}
+		break;
+	case 'd':
+		$deletes = array();
+		foreach ($data['old'] as $key=>$value) {
+			$deletes[] = array(
+				'key' => $key,
+				'value' => nl2br($value),
+			);
+		}
+		$app->tpl->setLoop('deletes', $deletes);
+		break;
+}
+
+$app->tpl->setVar($out);
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+
+function show_diff_if_needed($old, $new) {
+	global $app;
+
+	$diff_min_lines = 6;
+
+	if (substr_count($old, "\n") >= $diff_min_lines || substr_count($new, "\n") >= $diff_min_lines) {
+		$opcodes = FineDiff::getDiffOpcodes($old, $new);
+		$html = FineDiff::renderUTF8DiffToHTMLFromOpcodes($old, $opcodes);
+		return array('is_diff'=>true, 'old'=>'', 'new'=>'', 'diff'=>$html);
+	} else {
+		return array('is_diff'=>false, 'old'=>$old, 'new'=>$new, 'diff'=>'');
+	}
+}
+
+?>
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_list.lng b/interface/web/monitor/lib/lang/de_dataloghistory_list.lng
new file mode 100644
index 0000000000000000000000000000000000000000..2c2b6c9fc2d41c11f5d37d11d691140229718b7a
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_list.lng
@@ -0,0 +1,8 @@
+<?php
+$wb['list_head_txt'] = 'Datalog-History';
+$wb['tstamp_txt'] = 'Datum';
+$wb['server_id_txt'] = 'Server';
+$wb['dbtable_txt'] = 'Datenbanktabelle';
+$wb['action_txt'] = 'Aktion';
+$wb['status_txt'] = 'Status';
+?>
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng b/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng
new file mode 100644
index 0000000000000000000000000000000000000000..306f7c5eac0d7de9b3bd8b676a368af67a018fbc
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng
@@ -0,0 +1,6 @@
+<?php
+$wb['list_head_txt'] = 'Datalog-History-Eintrag';
+$wb['success_txt'] = 'Erfolgreich zurückgesetzt';
+$wb['error_txt'] = 'Fehler beim Zurücksetzen: Eintrag existiert nicht mehr';
+$wb['btn_cancel_txt'] = 'Zurück';
+?>
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_view.lng b/interface/web/monitor/lib/lang/de_dataloghistory_view.lng
new file mode 100644
index 0000000000000000000000000000000000000000..81123a69c0676d5163b68be44f347557bbd1086e
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_view.lng
@@ -0,0 +1,26 @@
+<?php
+$wb['i'] = 'Insert';
+$wb['u'] = 'Update';
+$wb['d'] = 'Delete';
+$wb['list_head_txt'] = 'Datalog-History-Eintrag';
+$wb['id_txt'] = 'ID';
+$wb['timestamp_txt'] = 'Zeitpunkt';
+$wb['table_txt'] = 'Tabelle';
+$wb['action_txt'] = 'Aktion';
+$wb['session_id_txt'] = 'Session-ID';
+$wb['fields_txt'] = 'Felder';
+$wb['fields_inserted_txt'] = 'Eingefügte Felder';
+$wb['fields_updated_txt'] = 'Geänderte Felder';
+$wb['fields_deleted_txt'] = 'Gelöschte Felder';
+$wb['no_changes_txt'] = 'Keine Änderungen (Resync)';
+$wb['is_diff_txt'] = 'Die Änderungen werden hervorgehoben angezeigt';
+$wb['is_diff_inserts_txt'] = 'Einfügungen';
+$wb['is_diff_deletes_txt'] = 'Löschungen';
+$wb['field_txt'] = 'Feld';
+$wb['value_txt'] = 'Wert';
+$wb['old_txt'] = 'Alt';
+$wb['new_txt'] = 'Neu';
+$wb['btn_cancel_txt'] = 'Zurück';
+$wb['undo_txt'] = 'Rückgängig machen';
+$wb['undo_confirmation_txt'] = 'Soll diese Änderung wirklich rückgängig gemacht werden?';
+?>
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_list.lng b/interface/web/monitor/lib/lang/en_dataloghistory_list.lng
new file mode 100644
index 0000000000000000000000000000000000000000..9f9afd6347ba4443cc46796810fb1c4a24106ccf
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_list.lng
@@ -0,0 +1,8 @@
+<?php
+$wb["list_head_txt"] = 'Datalog History';
+$wb["tstamp_txt"] = 'Date';
+$wb["server_id_txt"] = 'Server';
+$wb["dbtable_txt"] = 'DB Table';
+$wb["action_txt"] = 'Action';
+$wb["status_txt"] = 'Status';
+?>
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng b/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng
new file mode 100644
index 0000000000000000000000000000000000000000..258d8866966c781d0302373230bd7e5b2f0391b9
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng
@@ -0,0 +1,6 @@
+<?php
+$wb['list_head_txt'] = 'Data Log History Entry';
+$wb['success_txt'] = 'Undo successful';
+$wb['error_txt'] = 'Error during undo: Record does not exist anymore';
+$wb['btn_cancel_txt'] = 'Back';
+?>
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_view.lng b/interface/web/monitor/lib/lang/en_dataloghistory_view.lng
new file mode 100644
index 0000000000000000000000000000000000000000..df9ddd286f46e816e06132e7465929ab8dd87229
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_view.lng
@@ -0,0 +1,26 @@
+<?php
+$wb['i'] = 'Insert';
+$wb['u'] = 'Update';
+$wb['d'] = 'Delete';
+$wb['list_head_txt'] = 'Data Log History Entry';
+$wb['id_txt'] = 'ID';
+$wb['timestamp_txt'] = 'Timestamp';
+$wb['table_txt'] = 'Table';
+$wb['action_txt'] = 'Action';
+$wb['session_id_txt'] = 'Session ID';
+$wb['fields_txt'] = 'Fields';
+$wb['fields_inserted_txt'] = 'Inserted Fields';
+$wb['fields_updated_txt'] = 'Updated Fields';
+$wb['fields_deleted_txt'] = 'Deleted Fields';
+$wb['no_changes_txt'] = 'No changes (re-sync)';
+$wb['is_diff_txt'] = 'The differences are highlighted';
+$wb['is_diff_inserts_txt'] = 'Insertions';
+$wb['is_diff_deletes_txt'] = 'Deletions';
+$wb['field_txt'] = 'Field';
+$wb['value_txt'] = 'Value';
+$wb['old_txt'] = 'Old';
+$wb['new_txt'] = 'New';
+$wb['btn_cancel_txt'] = 'Back';
+$wb['undo_txt'] = 'Undo action';
+$wb['undo_confirmation_txt'] = 'Do you really want to undo this action?';
+?>
diff --git a/interface/web/monitor/list/dataloghistory.list.php b/interface/web/monitor/list/dataloghistory.list.php
new file mode 100644
index 0000000000000000000000000000000000000000..1757125e8d2b33690f4bf7522513ca5c0eb3c653
--- /dev/null
+++ b/interface/web/monitor/list/dataloghistory.list.php
@@ -0,0 +1,91 @@
+<?php
+/*
+    Datatypes:
+    - INTEGER
+    - DOUBLE
+    - CURRENCY
+    - VARCHAR
+    - TEXT
+    - DATE
+*/
+
+//* Name of list
+$liste['name']     = 'dataloghistory';
+
+//* Database table
+$liste['table']    = 'sys_datalog';
+
+//* Primary index column
+$liste['table_idx']   = 'datalog_id';
+
+//* Search Field Prefix
+$liste['search_prefix']  = 'search_';
+
+//* Records per page
+$liste['records_per_page']  = "15";
+
+//* Script file for listing
+$liste['file']    = 'dataloghistory_list.php';
+
+//* Script file to edit
+$liste['edit_file']   = 'dataloghistory_list.php';
+
+//* Script file to delete
+$liste['delete_file']  = 'dataloghistory_list.php';
+
+//* Paging template
+$liste['paging_tpl']  = 'templates/paging.tpl.htm';
+
+//* Enable auth
+$liste['auth']    = 'no';
+
+
+/*****************************************************
+* Suchfelder
+*****************************************************/
+
+$liste["item"][] = array( 'field'  => "tstamp",
+	'datatype' => "DATETIME",
+	'formtype' => "TEXT",
+	'op'  => "like",
+	'prefix' => "",
+	'suffix' => "",
+	'width'  => "",
+	'value'  => "");
+
+
+$liste['item'][] = array( 'field'  => 'server_id',
+	'datatype' => 'INTEGER',
+	'formtype' => 'SELECT',
+	'op'  => '=',
+	'prefix' => '',
+	'suffix' => '',
+	'datasource' => array (  'type' => 'SQL',
+		'querystring' => 'SELECT server_id,server_name FROM server WHERE {AUTHSQL} ORDER BY server_name',
+		'keyfield'=> 'server_id',
+		'valuefield'=> 'server_name'
+	),
+	'width'  => '',
+	'value'  => '');
+
+$liste["item"][] = array( 'field'  => "action",
+	'datatype' => "VARCHAR",
+	'formtype' => "SELECT",
+	'op'  => "=",
+	'prefix' => "",
+	'suffix' => "",
+	'width'  => "",
+	'value'  => array('i' => "Insert", 'u' => "Update", 'd' => "Delete"));
+
+
+$liste["item"][] = array( 'field'  => "dbtable",
+	'datatype' => "VARCHAR",
+	'formtype' => "TEXT",
+	'op'  => "like",
+	'prefix' => "%",
+	'suffix' => "%",
+	'width'  => "",
+	'value'  => "");
+
+
+?>
diff --git a/interface/web/monitor/templates/dataloghistory_list.htm b/interface/web/monitor/templates/dataloghistory_list.htm
new file mode 100644
index 0000000000000000000000000000000000000000..0cd3c4f4d27f52fa40977b3e00b80bf0c65cfbb4
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_list.htm
@@ -0,0 +1,54 @@
+<tmpl_if name="list_head_txt">
+<div class='page-header'>
+        <h1><tmpl_var name="list_head_txt"></h1>
+</div>
+</tmpl_if>
+<tmpl_if name="list_desc_txt"><p><tmpl_var name="list_desc_txt"></p></tmpl_if>
+
+
+        <p class="fieldset-legend"><tmpl_var name="list_head_txt"></p>
+            <div class="table-wrapper marginTop15">
+<table class="table">
+                <thead class="dark form-group-sm">
+                    <tr>
+                        <th data-column="tstamp"><tmpl_var name="tstamp_txt"></th>
+                        <th data-column="server_id"><tmpl_var name="server_id_txt"></th>
+                        <th data-column="action"><tmpl_var name="action_txt"></th>
+                        <th data-column="dbtable"><tmpl_var name="dbtable_txt"></th>
+                        <th class="small-col text-right">{tmpl_var name='search_limit'}</th>
+                    </tr>
+                    <tr>
+                        <td>&nbsp;</td>
+                        <td><select class="form-control" name="search_server_id">{tmpl_var name='search_server_id'}</select></td>
+                        <td><select class="form-control" name="search_action">{tmpl_var name='search_action'}</select></td>
+                        <td><input class="form-control" type="text" name="search_dbtable" value="{tmpl_var name='search_dbtable'}" /></td>
+                        <td class="text-right">
+                            <button type="button" class="btn btn-default formbutton-default formbutton-narrow" name="Filter" id="Filter" value="{tmpl_var name="filter_txt"}" data-submit-form="pageForm" data-form-action="monitor/dataloghistory_list.php"><span class="icon icon-filter"></span></button>
+                        </td>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tmpl_loop name="records">
+                        <tr>
+                            <td><a href="#" data-load-content="monitor/dataloghistory_view.php?id={tmpl_var name='id'}">{tmpl_var name="tstamp"}</a></td>
+                            <td><a href="#" data-load-content="monitor/dataloghistory_view.php?id={tmpl_var name='id'}">{tmpl_var name="server_id"}</a></td>
+                            <td><a href="#" data-load-content="monitor/dataloghistory_view.php?id={tmpl_var name='id'}">{tmpl_var name="action"}</a></td>
+                            <td><a href="#" data-load-content="monitor/dataloghistory_view.php?id={tmpl_var name='id'}">{tmpl_var name="dbtable"}</a></td>
+                            <td class="text-right"></td>
+                        </tr>
+                    </tmpl_loop>
+                    <tmpl_unless name="records">
+                        <tr class="tbl_row_noresults tbl_row_<tmpl_if name='__EVEN__'}even<tmpl_else>uneven</tmpl_if>">
+                            <td colspan="5">{tmpl_var name='globalsearch_noresults_text_txt'}</td>
+                        </tr>
+                    </tmpl_unless>
+                </tbody>
+                <tfoot>
+                    <tr>
+                        <td colspan="5"><tmpl_var name="paging"></td>
+                    </tr>
+                </tfoot>
+            </table>
+</div>
+        
+    </div>
diff --git a/interface/web/monitor/templates/dataloghistory_undo.htm b/interface/web/monitor/templates/dataloghistory_undo.htm
new file mode 100644
index 0000000000000000000000000000000000000000..997231a885ad3fb3e412658fabdd5ce7a62712de
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_undo.htm
@@ -0,0 +1,17 @@
+<tmpl_if name="list_head_txt">
+<div class='page-header'>
+        <h1><tmpl_var name="list_head_txt"></h1>
+</div>
+</tmpl_if>
+
+<tmpl_if name='success'>
+	<tmpl_var name='success_txt'>
+<tmpl_else>
+	<tmpl_var name='error_txt'>
+</tmpl_if>
+
+<div class="clear">
+	<div class="right">
+		<button class="btn btn-default formbutton-default" type="button" value="{tmpl_var name='btn_cancel_txt'}" data-load-content="monitor/dataloghistory_list.php">{tmpl_var name='btn_cancel_txt'}</button>
+	</div>
+</div>
diff --git a/interface/web/monitor/templates/dataloghistory_view.htm b/interface/web/monitor/templates/dataloghistory_view.htm
new file mode 100644
index 0000000000000000000000000000000000000000..0ce595e2652c90709d649bc0881210ca3856d5e0
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_view.htm
@@ -0,0 +1,124 @@
+<tmpl_if name="list_head_txt">
+<div class='page-header'>
+        <h1><tmpl_var name="list_head_txt"></h1>
+</div>
+</tmpl_if>
+<div class="table-wrapper marginTop15">
+	<table class="table">
+		<thead class="dark form-group-sm">
+			<tr>
+				<th width="25%"><tmpl_var name="id_txt"></th>
+				<th><tmpl_var name="id"></th>
+			</tr>
+		</thead>
+		<tbody>
+			<tr>
+				<td><tmpl_var name="timestamp_txt"></td>
+				<td><tmpl_var name="timestamp"></td>
+			</tr>
+			<tr>
+				<td><tmpl_var name="table_txt"></td>
+				<td><tmpl_var name="table"></td>
+			</tr>
+			<tr>
+				<td><tmpl_var name="action_txt"></td>
+				<td><tmpl_var name="action_name"></td>
+			</tr>
+			<tr>
+				<td><tmpl_var name="session_id_txt"></td>
+				<td><tmpl_var name="session_id"></td>
+			</tr>
+		</tbody>
+    </table>
+</div>
+
+<tmpl_if name='action_char' value='i'>
+	<h2><tmpl_var name="fields_inserted_txt"></h2>
+</tmpl_if>
+<tmpl_if name='action_char' value='u'>
+	<h2><tmpl_var name="fields_updated_txt"></h2>
+</tmpl_if>
+<tmpl_if name='action_char' value='d'>
+	<h2><tmpl_var name="fields_deleted_txt"></h2>
+</tmpl_if>
+
+<div class="table-wrapper marginTop15">
+	<table class="table">
+		<tmpl_if name='action_char' value='i'>
+			<thead class="dark form-group-sm">
+				<tr>
+					<th width="25%"><tmpl_var name="field_txt"></th>
+					<th><tmpl_var name="value_txt"></th>
+				</tr>
+			</thead>
+			<tmpl_loop name='inserts'>
+				<tr>
+					<td><tmpl_var name="key"></td>
+					<td><tmpl_var name="value"></td>
+				</tr>
+			</tmpl_loop>
+		</tmpl_if>
+		<tmpl_if name='action_char' value='u'>
+			<tmpl_if name='no_changes'>
+				<tbody>
+					<tr>
+						<td class="text-center"><h3><tmpl_var name="no_changes_txt"></h3></td>
+					</tr>
+				</tbody>
+			<tmpl_else>
+				<thead class="dark form-group-sm">
+					<tr>
+						<th width="25%"><tmpl_var name="field_txt"></th>
+						<th><tmpl_var name="old_txt"></th>
+						<th><tmpl_var name="new_txt"></th>
+					</tr>
+				</thead>
+				<tbody>
+				<tmpl_loop name='updates'>
+					<tmpl_if name='is_diff'>
+						<tr>
+							<td><tmpl_var name="key"></td>
+							<td colspan="2">
+								<div class="alert alert-warning"><tmpl_var name="is_diff_txt"> (<span class="finediff"><ins><tmpl_var name="is_diff_inserts_txt"></ins></span> / <span class="finediff"><del><tmpl_var name="is_diff_deletes_txt"></del></span>)</div>
+								<div class="finediff"><tmpl_var name="diff"></div>
+							</td>
+						</tr>
+					<tmpl_else>
+						<tr>
+							<td><tmpl_var name="key"></td>
+							<td><tmpl_var name="old"></td>
+							<td><tmpl_var name="new"></td>
+						</tr>
+					</tmpl_if>
+				</tmpl_loop>
+				</tbody>
+			</tmpl_if>
+		</tmpl_if>
+		<tmpl_if name='action_char' value='d'>
+			<thead class="dark form-group-sm">
+				<tr>
+					<th width="25%"><tmpl_var name="field_txt"></th>
+					<th><tmpl_var name="value_txt"></th>
+				</tr>
+			</thead>
+			<tbody>
+			<tmpl_loop name='deletes'>
+				<tr>
+					<td><tmpl_var name="key"></td>
+					<td><tmpl_var name="value"></td>
+				</tr>
+			</tmpl_loop>
+			</tbody>
+		</tmpl_if>
+
+    </table>
+</div>
+
+<div class="clear">
+	<div class="right">
+		<tmpl_if name='action_char' value='u'>
+			<a class="btn btn-default formbutton-danger formbutton-narrow" href="javascript: ISPConfig.confirm_action('monitor/dataloghistory_undo.php?id={tmpl_var name='id'}','{tmpl_var name='undo_confirmation_txt'}');"> {tmpl_var name='undo_txt'}</a>
+		</tmpl_if>
+		<button class="btn btn-default formbutton-default" type="button" value="{tmpl_var name='btn_cancel_txt'}" data-load-content="monitor/dataloghistory_list.php">{tmpl_var name='btn_cancel_txt'}</button>
+	</div>
+</div>