Index: /binary-improvements/7dtd-server-fixes/ModInfo.xml
===================================================================
--- /binary-improvements/7dtd-server-fixes/ModInfo.xml	(revision 249)
+++ /binary-improvements/7dtd-server-fixes/ModInfo.xml	(revision 250)
@@ -5,5 +5,5 @@
 		<Description value="Common functions" />
 		<Author value="Christian 'Alloc' Illy" />
-		<Version value="6" />
+		<Version value="7" />
 		<Website value="http://7dtd.illy.bz" />
 	</ModInfo>
Index: /binary-improvements/7dtd-server-fixes/src/PersistentData/InvItem.cs
===================================================================
--- /binary-improvements/7dtd-server-fixes/src/PersistentData/InvItem.cs	(revision 249)
+++ /binary-improvements/7dtd-server-fixes/src/PersistentData/InvItem.cs	(revision 250)
@@ -9,9 +9,12 @@
 		public string itemName;
 		public int count;
+		public int quality;
+		public InvItem[] parts;
 
-		public InvItem (string itemName, int count)
+		public InvItem (string itemName, int count, int quality = -1)
 		{
 			this.itemName = itemName;
 			this.count = count;
+			this.quality = quality;
 		}
 	}
Index: /binary-improvements/7dtd-server-fixes/src/PersistentData/Inventory.cs
===================================================================
--- /binary-improvements/7dtd-server-fixes/src/PersistentData/Inventory.cs	(revision 249)
+++ /binary-improvements/7dtd-server-fixes/src/PersistentData/Inventory.cs	(revision 250)
@@ -23,5 +23,5 @@
 				ProcessInv (bag, pdf.bag, pdf.id);
 				ProcessInv (belt, pdf.inventory, pdf.id);
-				ProcessEqu (pdf.equipment);
+				ProcessEqu (pdf.equipment, pdf.id);
 			}
 		}
@@ -30,30 +30,49 @@
 			target.Clear ();
 			for (int i = 0; i < sourceFields.Length; i++) {
-				if (sourceFields [i].count > 0) {
-					int count = sourceFields [i].count;
-					int maxAllowed = ItemClass.list [sourceFields [i].itemValue.type].Stacknumber.Value;
-					string name = ItemClass.list [sourceFields [i].itemValue.type].GetItemName ();
-
-					if (count > maxAllowed) {
-						Log.Out ("Player with ID " + id + " has stack for \"" + name + "\" greater than allowed (" + count + " > " + maxAllowed + ")");
-					}
-					target.Add (new InvItem (name, count));
-				} else {
-					target.Add (null);
+				InvItem item = CreateInvItem (sourceFields [i].itemValue, sourceFields [i].count, id);
+				if (item != null && sourceFields [i].itemValue.Parts != null) {
+					ProcessParts (sourceFields [i].itemValue.Parts, item, id);
 				}
+				target.Add (item);
 			}
 		}
 
-		private void ProcessEqu (Equipment sourceEquipment) {
+		private void ProcessEqu (Equipment sourceEquipment, int _playerId) {
 			equipment = new InvItem[sourceEquipment.GetSlotCount ()];
 			for (int i = 0; i < sourceEquipment.GetSlotCount (); i++) {
-				if (sourceEquipment.GetSlotItem (i) != null && !sourceEquipment.GetSlotItem (i).Equals (ItemValue.None)) {
-					int count = 1;
-					string name = ItemClass.list [sourceEquipment.GetSlotItem (i).type].GetItemName ();
+				equipment [i] = CreateInvItem (sourceEquipment.GetSlotItem (i), 1, _playerId);
+			}
+		}
 
-					equipment [i] = new InvItem (name, count);
+		private void ProcessParts (ItemValue[] _parts, InvItem _item, int _playerId) {
+			InvItem[] itemParts = new InvItem[_parts.Length];
+			for (int i = 0; i < _parts.Length; i++) {
+				InvItem partItem = CreateInvItem (_parts [i], 1, _playerId);
+				if (partItem != null && _parts [i].Parts != null) {
+					ProcessParts (_parts [i].Parts, partItem, _playerId);
+				}
+				itemParts [i] = partItem;
+			}
+			_item.parts = itemParts;
+		}
+
+		private InvItem CreateInvItem (ItemValue _itemValue, int _count, int _playerId) {
+			if (_count > 0 && _itemValue != null && !_itemValue.Equals (ItemValue.None)) {
+				int maxAllowed = ItemClass.list [_itemValue.type].Stacknumber.Value;
+				string name = ItemClass.list [_itemValue.type].GetItemName ();
+
+				if (_count > maxAllowed) {
+					Log.Out ("Player with ID " + _playerId + " has stack for \"" + name + "\" greater than allowed (" + _count + " > " + maxAllowed + ")");
+				}
+
+				InvItem item = null;
+				if (_itemValue.HasQuality) {
+					item = new InvItem (name, _count, _itemValue.Quality);
 				} else {
-					equipment [i] = null;
+					item = new InvItem (name, _count);
 				}
+				return item;
+			} else {
+				return null;
 			}
 		}
Index: /binary-improvements/AllocsCommands/Commands/Give.cs
===================================================================
--- /binary-improvements/AllocsCommands/Commands/Give.cs	(revision 249)
+++ /binary-improvements/AllocsCommands/Commands/Give.cs	(revision 250)
@@ -5,8 +5,6 @@
 namespace AllocsFixes.CustomCommands
 {
-	public class Give : ConsoleCmdAbstract
-	{
-		public override string GetDescription ()
-		{
+	public class Give : ConsoleCmdAbstract {
+		public override string GetDescription () {
 			return "give an item to a player (entity id or name)";
 		}
@@ -14,21 +12,21 @@
 		public override string GetHelp () {
 			return "Give an item to a player by dropping it in front of that player\n" +
-				   "Usage:\n" +
-				   "   give <name / entity id> <item name> <amount>\n" +
-				   "Either pass the full name of a player or his entity id (given by e.g. \"lpi\").\n" +
-				   "Item name has to be the exact name of an item as listed by \"listitems\".\n" +
-				   "Amount is the number of instances of this item to drop (as a single stack).";
+				"Usage:\n" +
+				"   give <name / entity id> <item name> <amount>\n" +
+				"   give <name / entity id> <item name> <amount> <quality>\n" +
+				"Either pass the full name of a player or his entity id (given by e.g. \"lpi\").\n" +
+				"Item name has to be the exact name of an item as listed by \"listitems\".\n" +
+				"Amount is the number of instances of this item to drop (as a single stack).\n" +
+				"Quality is the quality of the dropped items for items that have a quality.";
 		}
 
-		public override string[] GetCommands ()
-		{
+		public override string[] GetCommands () {
 			return new string[] { "give", string.Empty };
 		}
 
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo)
-		{
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
 			try {
-				if (_params.Count != 3) {
-					SdtdConsole.Instance.Output ("Wrong number of arguments, expected 3, found " + _params.Count + ".");
+				if (_params.Count != 3 && _params.Count != 4) {
+					SdtdConsole.Instance.Output ("Wrong number of arguments, expected 3 or 4, found " + _params.Count + ".");
 					return;
 				}
@@ -41,5 +39,5 @@
 				}
 
-				ItemValue iv = ItemList.Instance.GetItemValue (_params[1]);
+				ItemValue iv = ItemList.Instance.GetItemValue (_params [1]);
 				if (iv == null) {
 					SdtdConsole.Instance.Output ("Item not found.");
@@ -51,4 +49,18 @@
 					SdtdConsole.Instance.Output ("Amount is not an integer or not greater than zero.");
 					return;
+				}
+
+				if (_params.Count == 4) {
+					if (!iv.HasQuality) {
+						SdtdConsole.Instance.Output ("Item " + _params [1] + " does not support quality.");
+						return;
+					}
+
+					int quality = int.MinValue;
+					if (!int.TryParse (_params [3], out quality) || quality <= 0) {
+						SdtdConsole.Instance.Output ("Quality is not an integer or not greater than zero.");
+						return;
+					}
+					iv.Quality = quality;
 				}
 
Index: /binary-improvements/AllocsCommands/Commands/ShowInventory.cs
===================================================================
--- /binary-improvements/AllocsCommands/Commands/ShowInventory.cs	(revision 249)
+++ /binary-improvements/AllocsCommands/Commands/ShowInventory.cs	(revision 250)
@@ -5,8 +5,6 @@
 namespace AllocsFixes.CustomCommands
 {
-	public class ShowInventory : ConsoleCmdAbstract
-	{
-		public override string GetDescription ()
-		{
+	public class ShowInventory : ConsoleCmdAbstract {
+		public override string GetDescription () {
 			return "list inventory of a given player";
 		}
@@ -14,18 +12,16 @@
 		public override string GetHelp () {
 			return "Usage:\n" +
-				   "   showinventory <steam id / player name / entity id>\n" +
-				   "Show the inventory of the player given by his SteamID, player name or\n" +
-				   "entity id (as given by e.g. \"lpi\")." +
-				   "Note: This only shows the player's inventory after it was first sent to\n" +
-				   "the server which happens at least every 30 seconds.";
+				"   showinventory <steam id / player name / entity id>\n" +
+				"Show the inventory of the player given by his SteamID, player name or\n" +
+				"entity id (as given by e.g. \"lpi\")." +
+				"Note: This only shows the player's inventory after it was first sent to\n" +
+				"the server which happens at least every 30 seconds.";
 		}
 
-		public override string[] GetCommands ()
-		{
+		public override string[] GetCommands () {
 			return new string[] { "showinventory", "si" };
 		}
 
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo)
-		{
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
 			try {
 				if (_params.Count < 1) {
@@ -44,19 +40,77 @@
 
 				SdtdConsole.Instance.Output ("Belt of player " + p.Name + ":");
-				for (int i = 0; i < inv.belt.Count; i++) {
-					if (inv.belt [i] != null)
-						SdtdConsole.Instance.Output (string.Format ("    Slot {0}: {1:000} * {2}", i, inv.belt [i].count, inv.belt [i].itemName));
-				}
+				PrintInv (inv.belt);
 				SdtdConsole.Instance.Output (string.Empty);
+
 				SdtdConsole.Instance.Output ("Bagpack of player " + p.Name + ":");
-				for (int i = 0; i < inv.bag.Count; i++) {
-					if (inv.bag [i] != null)
-						SdtdConsole.Instance.Output (string.Format ("    Slot {0}: {1:000} * {2}", i, inv.bag [i].count, inv.bag [i].itemName));
-				}
+				PrintInv (inv.bag);
 				SdtdConsole.Instance.Output (string.Empty);
+
+				SdtdConsole.Instance.Output ("Equipment of player " + p.Name + ":");
+				PrintEquipment (inv.equipment);
+
 			} catch (Exception e) {
 				Log.Out ("Error in ShowInventory.Run: " + e);
 			}
 		}
+
+		private void PrintInv (List<InvItem> _inv) {
+			for (int i = 0; i < _inv.Count; i++) {
+				if (_inv [i] != null) {
+					if (_inv [i].quality < 0) {
+						SdtdConsole.Instance.Output (string.Format ("    Slot {0}: {1:000} * {2}", i, _inv [i].count, _inv [i].itemName));
+					} else {
+						SdtdConsole.Instance.Output (string.Format ("    Slot {0}: {1:000} * {2} - quality: {3}", i, _inv [i].count, _inv [i].itemName, _inv [i].quality));
+					}
+					DoParts (_inv [i].parts, 1);
+				}
+			}
+		}
+
+		private void PrintEquipment (InvItem[] _equipment) {
+			AddEquipment ("head", _equipment, XMLData.Item.EnumEquipmentSlot.Head, NGuiInvGridEquipment.EnumClothingLayer.Middle);
+			AddEquipment ("eyes", _equipment, XMLData.Item.EnumEquipmentSlot.Eyes, NGuiInvGridEquipment.EnumClothingLayer.Middle);
+			AddEquipment ("face", _equipment, XMLData.Item.EnumEquipmentSlot.Face, NGuiInvGridEquipment.EnumClothingLayer.Middle);
+
+			AddEquipment ("armor", _equipment, XMLData.Item.EnumEquipmentSlot.Chest, NGuiInvGridEquipment.EnumClothingLayer.Outer);
+			AddEquipment ("jacket", _equipment, XMLData.Item.EnumEquipmentSlot.Chest, NGuiInvGridEquipment.EnumClothingLayer.Middle);
+			AddEquipment ("shirt", _equipment, XMLData.Item.EnumEquipmentSlot.Chest, NGuiInvGridEquipment.EnumClothingLayer.Inner);
+
+			AddEquipment ("legarmor", _equipment, XMLData.Item.EnumEquipmentSlot.Legs, NGuiInvGridEquipment.EnumClothingLayer.Outer);
+			AddEquipment ("pants", _equipment, XMLData.Item.EnumEquipmentSlot.Legs, NGuiInvGridEquipment.EnumClothingLayer.Inner);
+			AddEquipment ("boots", _equipment, XMLData.Item.EnumEquipmentSlot.Feet, NGuiInvGridEquipment.EnumClothingLayer.Inner);
+
+			AddEquipment ("gloves", _equipment, XMLData.Item.EnumEquipmentSlot.Hands, NGuiInvGridEquipment.EnumClothingLayer.Inner);
+			AddEquipment ("backpack", _equipment, XMLData.Item.EnumEquipmentSlot.Back, NGuiInvGridEquipment.EnumClothingLayer.Outer);
+		}
+
+		private void AddEquipment (string _slotname, InvItem[] _items, XMLData.Item.EnumEquipmentSlot _slot, NGuiInvGridEquipment.EnumClothingLayer _layer) {
+			int index = (int)_slot + (int)_layer * (int)XMLData.Item.EnumEquipmentSlot.Count;
+			if (_items != null && _items [index] != null) {
+				if (_items [index].quality < 0) {
+					SdtdConsole.Instance.Output (string.Format ("    Slot {0:8}: {1:000}", _slotname, _items [index].itemName));
+				} else {
+					SdtdConsole.Instance.Output (string.Format ("    Slot {0:8}: {1:000} - quality: {2}", _slotname, _items [index].itemName, _items [index].quality));
+				}
+				DoParts (_items [index].parts, 1);
+			}
+		}
+
+		private void DoParts (InvItem[] _parts, int _indent) {
+			if (_parts != null && _parts.Length > 0) {
+				string indenter = new string (' ', _indent * 4);
+				for (int i = 0; i < _parts.Length; i++) {
+					if (_parts [i] != null) {
+						if (_parts [i].quality < 0) {
+							SdtdConsole.Instance.Output (string.Format ("{0}         - {1}", indenter, _parts [i].itemName));
+						} else {
+							SdtdConsole.Instance.Output (string.Format ("{0}         - {1} - quality: {2}", indenter, _parts [i].itemName, _parts [i].quality));
+						}
+						DoParts (_parts [i].parts, _indent + 1);
+					}
+				}
+			}
+		}
+
 	}
 }
Index: /binary-improvements/AllocsCommands/Commands/TeleportPlayer.cs
===================================================================
--- /binary-improvements/AllocsCommands/Commands/TeleportPlayer.cs	(revision 249)
+++ /binary-improvements/AllocsCommands/Commands/TeleportPlayer.cs	(revision 250)
@@ -5,8 +5,6 @@
 namespace AllocsFixes.CustomCommands
 {
-	public class TeleportPlayer : ConsoleCmdAbstract
-	{
-		public override string GetDescription ()
-		{
+	public class TeleportPlayer : ConsoleCmdAbstract {
+		public override string GetDescription () {
 			return "teleport a player to a given location";
 		}
@@ -14,38 +12,79 @@
 		public override string GetHelp () {
 			return "Usage:\n" +
-				   "  1. teleportplayer <steam id / player name / entity id> <x> <y> <z>\n" +
-				   "  2. teleportplayer <steam id / player name / entity id> <target steam id / player name / entity id>\n" +
-				   "1. Teleports the player given by his SteamID, player name or entity id (as given by e.g. \"lpi\")\n" +
-				   "   to the specified location\n" +
-					"2. As 1, but destination given by another player which has to be online";
+				"  1. teleportplayer <steam id / player name / entity id> <x> <y> <z>\n" +
+				"  2. teleportplayer <steam id / player name / entity id> <target steam id / player name / entity id>\n" +
+				"  3. teleportplayer <inc x> <inc y> <inc z>\n" +
+				"1. Teleports the player given by his SteamID, player name or entity id (as given by e.g. \"lpi\")\n" +
+				"   to the specified location\n" +
+				"2. As 1, but destination given by another player which has to be online\n" +
+				"3. Teleport the local player to the position calculated by his current position and the given offsets";
 		}
 
-		public override string[] GetCommands ()
-		{
+		public override string[] GetCommands () {
 			return new string[] { "teleportplayer", "tele" };
 		}
 
-		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo)
-		{
+		public override void Execute (List<string> _params, CommandSenderInfo _senderInfo) {
 			try {
-				if (_params.Count != 4 && _params.Count != 2) {
-					SdtdConsole.Instance.Output ("Usage: teleportplayer <entityid|playername|steamid> <x> <y> <z>");
-					SdtdConsole.Instance.Output ("   or: teleportplayer <entityid|playername|steamid> <target entityid|playername|steamid>");
+				if (_params.Count < 2 || _params.Count > 4) {
+					SdtdConsole.Instance.Output ("Wrong number of arguments, expected 2 to 4, found " + _params.Count + ".");
+					return;
 				} else {
-					ClientInfo ci1 = ConsoleHelper.ParseParamIdOrName (_params [0]);
-					if (ci1 == null) {
-						SdtdConsole.Instance.Output ("Playername or entity/steamid id not found.");
-						return;
-					}
-					EntityPlayer ep1 = GameManager.Instance.World.Players.dict [ci1.entityId];
+					ClientInfo ci1 = null;
+					EntityPlayer ep1 = null;
+ 
+					if (_params.Count == 2 || _params.Count == 4) {
+						ci1 = ConsoleHelper.ParseParamIdOrName (_params [0]);
+						if (ci1 == null) {
+							SdtdConsole.Instance.Output ("Playername or entity/steamid id not found.");
+							return;
+						}
+						ep1 = GameManager.Instance.World.Players.dict [ci1.entityId];
 
-					if (_params.Count == 4) {
+						if (_params.Count == 4) {
+							int x = int.MinValue;
+							int y = int.MinValue;
+							int z = int.MinValue;
+
+							int.TryParse (_params [1], out x);
+							int.TryParse (_params [2], out y);
+							int.TryParse (_params [3], out z);
+
+							if (x == int.MinValue || y == int.MinValue || z == int.MinValue) {
+								SdtdConsole.Instance.Output ("At least one of the given coordinates is not a valid integer");
+								return;
+							}
+
+							ep1.position.x = x;
+							ep1.position.y = y;
+							ep1.position.z = z;
+						} else if (_params.Count == 2) {
+							ClientInfo ci2 = ConsoleHelper.ParseParamIdOrName (_params [1]);
+							if (ci2 == null) {
+								SdtdConsole.Instance.Output ("Target playername or entity/steamid id not found.");
+								return;
+							}
+							EntityPlayer ep2 = GameManager.Instance.World.Players.dict [ci2.entityId];
+
+							ep1.position = ep2.GetPosition ();
+							ep1.position.y += 1;
+							ep1.position.z += 1;
+						}
+					} else if (_params.Count == 3) {
+						if (_senderInfo.RemoteClientInfo == null) {
+							SdtdConsole.Instance.Output ("This command can only be executed on the in-game console.");
+							return;
+						}
+
+						ci1 = _senderInfo.RemoteClientInfo;
+						ep1 = GameManager.Instance.World.Players.dict [ci1.entityId];
+
 						int x = int.MinValue;
 						int y = int.MinValue;
 						int z = int.MinValue;
 
-						int.TryParse (_params [1], out x);
-						int.TryParse (_params [2], out y);
-						int.TryParse (_params [3], out z);
+						int.TryParse (_params [0], out x);
+						int.TryParse (_params [1], out y);
+						int.TryParse (_params [2], out z);
 
 						if (x == int.MinValue || y == int.MinValue || z == int.MinValue) {
@@ -54,18 +93,7 @@
 						}
 
-						ep1.position.x = x;
-						ep1.position.y = y;
-						ep1.position.z = z;
-					} else {
-						ClientInfo ci2 = ConsoleHelper.ParseParamIdOrName (_params [1]);
-						if (ci2 == null) {
-							SdtdConsole.Instance.Output ("Target playername or entity/steamid id not found.");
-							return;
-						}
-						EntityPlayer ep2 = GameManager.Instance.World.Players.dict [ci2.entityId];
-
-						ep1.position = ep2.GetPosition();
-						ep1.position.y += 1;
-						ep1.position.z += 1;
+						ep1.position.x = ep1.position.x + x;
+						ep1.position.y = ep1.position.y + y;
+						ep1.position.z = ep1.position.z + z;
 					}
 
Index: /binary-improvements/AllocsCommands/ModInfo.xml
===================================================================
--- /binary-improvements/AllocsCommands/ModInfo.xml	(revision 249)
+++ /binary-improvements/AllocsCommands/ModInfo.xml	(revision 250)
@@ -5,5 +5,5 @@
 		<Description value="Additional commands for server operation" />
 		<Author value="Christian 'Alloc' Illy" />
-		<Version value="4" />
+		<Version value="5" />
 		<Website value="http://7dtd.illy.bz" />
 	</ModInfo>
Index: /binary-improvements/MapRendering/API.cs
===================================================================
--- /binary-improvements/MapRendering/API.cs	(revision 249)
+++ /binary-improvements/MapRendering/API.cs	(revision 250)
@@ -7,4 +7,5 @@
 		public override void GameAwake () {
 			new AllocsFixes.NetConnections.Servers.Web.Web ();
+			AllocsFixes.NetConnections.Servers.Web.LogBuffer.Instance.GetType ();
 		}
 
Index: /binary-improvements/MapRendering/ModInfo.xml
===================================================================
--- /binary-improvements/MapRendering/ModInfo.xml	(revision 249)
+++ /binary-improvements/MapRendering/ModInfo.xml	(revision 250)
@@ -5,5 +5,5 @@
 		<Description value="Render the game map to image map tiles as it is uncovered" />
 		<Author value="Christian 'Alloc' Illy" />
-		<Version value="8" />
+		<Version value="9" />
 		<Website value="http://7dtd.illy.bz" />
 	</ModInfo>
Index: /binary-improvements/MapRendering/Web/API/GetLog.cs
===================================================================
--- /binary-improvements/MapRendering/Web/API/GetLog.cs	(revision 250)
+++ /binary-improvements/MapRendering/Web/API/GetLog.cs	(revision 250)
@@ -0,0 +1,45 @@
+using AllocsFixes.JSON;
+using AllocsFixes.PersistentData;
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.API
+{
+	public class GetLog : WebAPI {
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			int firstLine, lastLine;
+
+			if (req.QueryString ["firstLine"] == null || !int.TryParse (req.QueryString ["firstLine"], out firstLine)) {
+				firstLine = LogBuffer.Instance.OldestLine;
+			}
+
+			if (req.QueryString ["lastLine"] == null || !int.TryParse (req.QueryString ["lastLine"], out lastLine)) {
+				lastLine = -1;
+			}
+
+			JSONObject result = new JSONObject ();
+
+			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, ref lastLine);
+
+			JSONArray entries = new JSONArray ();
+			foreach (LogBuffer.LogEntry logEntry in logEntries) {
+				JSONObject entry = new JSONObject ();
+				entry.Add ("date", new JSONString (logEntry.date));
+				entry.Add ("time", new JSONString (logEntry.time));
+				entry.Add ("uptime", new JSONString (logEntry.uptime));
+				entry.Add ("msg", new JSONString (logEntry.message));
+				entry.Add ("trace", new JSONString (logEntry.trace));
+				entry.Add ("type", new JSONString (logEntry.type.ToString ()));
+				entries.Add (entry);
+			}
+
+			result.Add ("firstLine", new JSONNumber (firstLine));
+			result.Add ("lastLine", new JSONNumber (lastLine));
+			result.Add ("entries", entries);
+
+			WriteJSON (resp, result);
+		}
+	}
+}
+
Index: /binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs
===================================================================
--- /binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 249)
+++ /binary-improvements/MapRendering/Web/API/GetPlayerInventory.cs	(revision 250)
@@ -35,26 +35,6 @@
 			result.Add ("equipment", equipment);
 
-			for (int i = 0; i < inv.belt.Count; i++) {
-				JSONObject item = new JSONObject ();
-				if (inv.belt [i] != null) {
-					item.Add ("count", new JSONNumber (inv.belt [i].count));
-					item.Add ("name", new JSONString (inv.belt [i].itemName));
-				} else {
-					item.Add ("count", new JSONNumber (0));
-					item.Add ("name", new JSONString (string.Empty));
-				}
-				belt.Add (item);
-			}
-			for (int i = 0; i < inv.bag.Count; i++) {
-				JSONObject item = new JSONObject ();
-				if (inv.bag [i] != null) {
-					item.Add ("count", new JSONNumber (inv.bag [i].count));
-					item.Add ("name", new JSONString (inv.bag [i].itemName));
-				} else {
-					item.Add ("count", new JSONNumber (0));
-					item.Add ("name", new JSONString (string.Empty));
-				}
-				bag.Add (item);
-			}
+			DoInventory (belt, inv.belt);
+			DoInventory (bag, inv.bag);
 
 			AddEquipment (equipment, "head", inv.equipment, XMLData.Item.EnumEquipmentSlot.Head, NGuiInvGridEquipment.EnumClothingLayer.Middle);
@@ -76,10 +56,31 @@
 		}
 
+		private void DoInventory (JSONArray _jsonRes, List<InvItem> _inv) {
+			for (int i = 0; i < _inv.Count; i++) {
+				_jsonRes.Add (GetJsonForItem (_inv [i]));
+			}
+		}
+
 		private void AddEquipment (JSONObject _eq, string _slotname, InvItem[] _items, XMLData.Item.EnumEquipmentSlot _slot, NGuiInvGridEquipment.EnumClothingLayer _layer) {
 			int index = (int)_slot + (int)_layer * (int)XMLData.Item.EnumEquipmentSlot.Count;
-			if (_items != null && _items [index] != null) {
-				_eq.Add (_slotname, new JSONString (_items [index].itemName));
+			if (_items != null) {
+				_eq.Add (_slotname, GetJsonForItem (_items [index]));
 			} else {
-				_eq.Add (_slotname, new JSONBoolean (false));
+				_eq.Add (_slotname, new JSONNull ());
+			}
+		}
+
+		private JSONNode GetJsonForItem (InvItem _item) {
+			if (_item != null) {
+				JSONObject jsonItem = new JSONObject ();
+				jsonItem.Add ("count", new JSONNumber (_item.count));
+				jsonItem.Add ("name", new JSONString (_item.itemName));
+				jsonItem.Add ("quality", new JSONNumber (_item.quality));
+				if (_item.quality >= 0) {
+					jsonItem.Add ("qualitycolor", new JSONString (QualityInfo.GetQualityColorHex (_item.quality)));
+				}
+				return jsonItem;
+			} else {
+				return new JSONNull ();
 			}
 		}
Index: /binary-improvements/MapRendering/Web/API/GetWebUIUpdates.cs
===================================================================
--- /binary-improvements/MapRendering/Web/API/GetWebUIUpdates.cs	(revision 250)
+++ /binary-improvements/MapRendering/Web/API/GetWebUIUpdates.cs	(revision 250)
@@ -0,0 +1,32 @@
+using AllocsFixes.JSON;
+using AllocsFixes.PersistentData;
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace AllocsFixes.NetConnections.Servers.Web.API
+{
+	public class GetWebUIUpdates : WebAPI {
+		public override void HandleRequest (HttpListenerRequest req, HttpListenerResponse resp, WebConnection user, int permissionLevel) {
+			int latestLine;
+			if (req.QueryString ["latestLine"] == null || !int.TryParse (req.QueryString ["latestLine"], out latestLine)) {
+				latestLine = 0;
+			}
+
+			JSONObject result = new JSONObject ();
+
+			JSONObject time = new JSONObject ();
+			time.Add ("days", new JSONNumber (GameUtils.WorldTimeToDays (GameManager.Instance.World.worldTime)));
+			time.Add ("hours", new JSONNumber (GameUtils.WorldTimeToHours (GameManager.Instance.World.worldTime)));
+			time.Add ("minutes", new JSONNumber (GameUtils.WorldTimeToMinutes (GameManager.Instance.World.worldTime)));
+			result.Add ("gametime", time);
+
+			result.Add ("players", new JSONNumber (GameManager.Instance.World.Players.Count));
+
+			result.Add ("newlogs", new JSONNumber (LogBuffer.Instance.LatestLine - latestLine));
+
+			WriteJSON (resp, result);
+		}
+	}
+}
+
Index: /binary-improvements/MapRendering/Web/ConnectionHandler.cs
===================================================================
--- /binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 249)
+++ /binary-improvements/MapRendering/Web/ConnectionHandler.cs	(revision 250)
@@ -50,10 +50,4 @@
 		}
 
-		public void SendLog (string text, string trace, UnityEngine.LogType type) {
-			foreach (WebConnection wc in connections.Values) {
-				wc.SendLog (text, trace, type);
-			}
-		}
-
 	}
 }
Index: /binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs
===================================================================
--- /binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 249)
+++ /binary-improvements/MapRendering/Web/Handlers/ApiHandler.cs	(revision 250)
@@ -4,4 +4,5 @@
 using System.IO;
 using System.Net;
+using System.Reflection;
 using System.Threading;
 
@@ -14,9 +15,14 @@
 		public ApiHandler (string staticPart, string moduleName = null) : base(moduleName) {
 			this.staticPart = staticPart;
-			addApi ("getlandclaims", new GetLandClaims ());
-			addApi ("getplayersonline", new GetPlayersOnline ());
-			addApi ("getplayerslocation", new GetPlayersLocation ());
-			addApi ("getplayerinventory", new GetPlayerInventory ());
-			addApi ("getstats", new GetStats ());
+
+			foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) {
+				if (!t.IsAbstract && t.IsSubclassOf (typeof(WebAPI))) {
+					ConstructorInfo ctor = t.GetConstructor (new Type[0]);
+					if (ctor != null) {
+						WebAPI apiInstance = (WebAPI)ctor.Invoke (new object[0]);
+						addApi (t.Name.ToLower (), apiInstance);
+					}
+				}
+			}
 		}
 
@@ -38,13 +44,14 @@
 			} else {
 				foreach (KeyValuePair<string, WebAPI> kvp in apis) {
-					try {
-						if (apiName.StartsWith (kvp.Key)) {
+					if (apiName.StartsWith (kvp.Key)) {
+						try {
 							kvp.Value.HandleRequest (req, resp, user, permissionLevel);
 							return;
+						} catch (Exception e) {
+							Log.Error ("Error in ApiHandler.HandleRequest(): Handler {0} threw an exception:", kvp.Key);
+							Log.Exception (e);
+							resp.StatusCode = (int)HttpStatusCode.InternalServerError;
+							return;
 						}
-					} catch (Exception e) {
-						Log.Out ("Error in ApiHandler.HandleRequest(): Handler threw an exception: " + e);
-						resp.StatusCode = (int)HttpStatusCode.InternalServerError;
-						return;
 					}
 				}
Index: /binary-improvements/MapRendering/Web/LogBuffer.cs
===================================================================
--- /binary-improvements/MapRendering/Web/LogBuffer.cs	(revision 250)
+++ /binary-improvements/MapRendering/Web/LogBuffer.cs	(revision 250)
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+using UnityEngine;
+
+namespace AllocsFixes.NetConnections.Servers.Web
+{
+	public class LogBuffer {
+		private const int MAX_ENTRIES = 50000;
+		private static LogBuffer instance;
+
+		public static LogBuffer Instance {
+			get {
+				if (instance == null) {
+					instance = new LogBuffer ();
+				}
+				return instance;
+			}
+		}
+
+		private static Regex logMessageMatcher = new Regex (@"^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}) ([0-9]+[,.][0-9]+) [A-Z]+ (.*)$");
+		private List<LogEntry> logEntries = new List<LogEntry> ();
+		private int listOffset = 0;
+
+		public int OldestLine {
+			get {
+				lock (logEntries) {
+					return listOffset;
+				}
+			}
+		}
+
+		public int LatestLine {
+			get {
+				lock (logEntries) {
+					return listOffset + logEntries.Count - 1;
+				}
+			}
+		}
+
+		public int StoredLines {
+			get {
+				lock (logEntries) {
+					return logEntries.Count;
+				}
+			}
+		}
+
+		public LogEntry this [int index] {
+			get {
+				lock (logEntries) {
+					if (index >= listOffset && index < listOffset + logEntries.Count) {
+						return logEntries [index];
+					}
+				}
+				return null;
+			}
+		}
+
+		private LogBuffer () {
+			Logger.Main.LogCallbacks += LogCallback;
+		}
+
+		private void LogCallback (string _msg, string _trace, LogType _type) {
+			LogEntry le = new LogEntry ();
+
+			Match match = logMessageMatcher.Match (_msg);
+			if (match.Success) {
+				le.date = match.Groups [1].Value;
+				le.time = match.Groups [2].Value;
+				le.uptime = match.Groups [3].Value;
+				le.message = match.Groups [4].Value;
+			} else {
+				DateTime dt = DateTime.Now;
+				le.date = string.Format ("{0:0000}-{1:00}-{2:00}", dt.Year, dt.Month, dt.Day);
+				le.time = string.Format ("{0:00}:{1:00}:{2:00}", dt.Hour, dt.Minute, dt.Second);
+				le.uptime = "";
+				le.message = _msg;
+			}
+
+			le.trace = _trace;
+			le.type = _type;
+
+			lock (logEntries) {
+				logEntries.Add (le);
+				if (logEntries.Count > MAX_ENTRIES) {
+					listOffset += logEntries.Count - MAX_ENTRIES;
+					logEntries.RemoveRange (0, logEntries.Count - MAX_ENTRIES);
+				}
+			}
+		}
+
+		public List<LogEntry> GetRange (ref int _start, ref int _end) {
+			lock (logEntries) {
+				if (_end < 0) {
+					_end = listOffset + logEntries.Count;
+				}
+
+				if (_start < listOffset) {
+					_start = listOffset;
+				}
+
+				if (_start >= listOffset + logEntries.Count) {
+					_end = _start;
+					return new List<LogEntry> ();
+				}
+
+				if (_end < _start) {
+					Log.Error ("GetRange: invalid end {0} (listOffset: {1}, count: {2})", _end, listOffset, logEntries.Count);
+					return null;
+				}
+
+				if (_end >= listOffset + logEntries.Count) {
+					_end = listOffset + logEntries.Count - 1;
+				}
+
+				return logEntries.GetRange (_start - listOffset, _end - _start + 1);
+			}
+		}
+
+
+		public class LogEntry {
+			public string date;
+			public string time;
+			public string uptime;
+			public string message;
+			public string trace;
+			public LogType type;
+		}
+	}
+}
+
Index: /binary-improvements/MapRendering/Web/Web.cs
===================================================================
--- /binary-improvements/MapRendering/Web/Web.cs	(revision 249)
+++ /binary-improvements/MapRendering/Web/Web.cs	(revision 250)
@@ -250,5 +250,5 @@
 
 		public void SendLog (string text, string trace, UnityEngine.LogType type) {
-			connectionHandler.SendLog (text, trace, type);
+			// Do nothing, handled by LogBuffer internally
 		}
 
Index: /binary-improvements/MapRendering/Web/WebConnection.cs
===================================================================
--- /binary-improvements/MapRendering/Web/WebConnection.cs	(revision 249)
+++ /binary-improvements/MapRendering/Web/WebConnection.cs	(revision 250)
@@ -12,7 +12,5 @@
 		private DateTime login;
 		private DateTime lastAction;
-
 		private List<string> outputLines = new List<string> ();
-		private List<LogLine> logLines = new List<LogLine> ();
 
 		public string SessionID {
@@ -57,16 +55,7 @@
 
 		public override void SendLog (string _msg, string _trace, LogType _type) {
-			LogLine ll = new LogLine ();
-			ll.message = _msg;
-			ll.trace = _trace;
-			ll.type = _type;
-			logLines.Add (ll);
+			// Do nothing, handled by LogBuffer
 		}
 
-		private struct LogLine {
-			public string message;
-			public string trace;
-			public LogType type;
-		}
 	}
 }
Index: /binary-improvements/MapRendering/WebAndMapRendering.csproj
===================================================================
--- /binary-improvements/MapRendering/WebAndMapRendering.csproj	(revision 249)
+++ /binary-improvements/MapRendering/WebAndMapRendering.csproj	(revision 250)
@@ -75,4 +75,7 @@
     <Compile Include="Commands\WebTokens.cs" />
     <Compile Include="Commands\WebPermissionsCmd.cs" />
+    <Compile Include="Web\LogBuffer.cs" />
+    <Compile Include="Web\API\GetLog.cs" />
+    <Compile Include="Web\API\GetWebUIUpdates.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
Index: /binary-improvements/webserver/css/style.css
===================================================================
--- /binary-improvements/webserver/css/style.css	(revision 249)
+++ /binary-improvements/webserver/css/style.css	(revision 250)
@@ -14,10 +14,10 @@
 
 a {
-	color: #ff6000;
+	color: orangered;
 	text-decoration: none;
 }
 
 a:visited {
-	color: #ff6000;
+	color: orangered;
 	text-decoration: none;
 }
@@ -30,5 +30,5 @@
 
 .adminnavbar,
-.admincontent {
+#admincontent {
 	position: absolute;
 	top: 0;
@@ -50,4 +50,5 @@
 	border-right: 1px solid rgba(0,0,0,0.3);
 	box-shadow: 3px 0px 14px rgba(0,0,0,0.9);
+	position: fixed;
 }
 
@@ -69,5 +70,5 @@
 
 .adminnavbar #adminmenu .menu_button.allowed {
-	display: inline;
+	display: list-item;
 }
 
@@ -80,4 +81,17 @@
 	font-weight: bold;
 	text-transform: uppercase;
+}
+
+#newlogcount {
+	font-size: 70%;
+	border-radius: 2px;
+	background-color: #f00;
+	color: #fff;
+	padding: 0px 2px 0px 2px;
+	display: none;
+}
+
+#newlogcount.visible {
+	display: inline;
 }
 
@@ -122,5 +136,5 @@
 */
 
-.admincontent {
+#admincontent {
 	position: absolute;
 	right: 0;
@@ -129,9 +143,9 @@
 }
 
-.admincontent #nopermissionwarning {
+#admincontent #nopermissionwarning {
 	margin: 20px 50px;
 }
 
-.admincontent .contenttab {
+#admincontent .contenttab {
 	position: absolute;
 	top: 0;
@@ -141,5 +155,5 @@
 }
 
-.admincontent .current_tab {
+#admincontent .current_tab {
 	display: block;
 }
@@ -199,7 +213,5 @@
 	border-collapse: collapse;
 }
-#equipmentTable .invFieldText {
-	display: none;
-}
+
 .playerInventoryDialog td.invField {
 	width: 58px;
@@ -222,4 +234,24 @@
 }
 
+#equipmentTable .invFieldText {
+	display: none;
+}
+.playerInventoryDialog .invFieldText { 
+	display: none;
+}
+.playerInventoryDialog .invFieldText.visible { 
+	display: inline;
+}
+.playerInventoryDialog .invFieldQuality {
+	bottom: 0px;
+	height: 5px;
+	left: 0px;
+	position: relative;
+	right: 0px;
+	display: none;
+}
+.playerInventoryDialog .invFieldQuality.visible {
+	display: block;
+}
 
 
@@ -246,5 +278,5 @@
 .adminmap .leaflet-container a:hover {
 	text-decoration: none;
-	color: #ff6000;
+	color: orangered;
 }
 
@@ -261,2 +293,95 @@
 }
 
+
+
+
+/*========================================
+-   Log
+*/
+
+.adminlog {
+	padding: 10px;
+}
+
+.adminlog table {
+	width: 100%;
+}
+
+.adminlog table td {
+	vertical-align: top;
+}
+
+.adminlog table tr.readmark td {
+	border-bottom-width: 2px;
+	border-bottom-color: red;
+	border-bottom-style: dotted;
+}
+
+.adminlog table tr.Log td {
+	color: limegreen;
+}
+.adminlog table tr.Warning td {
+	color: orange;
+}
+.adminlog table tr.Error td {
+	color: red;
+}
+.adminlog table tr.Exception td {
+	color: red;
+}
+
+
+.adminlog .logcol_datetime,
+.adminlog .logcol_uptime {
+	white-space: nowrap;
+	text-align: right;
+}
+.adminlog .logcol_type {
+	white-space: nowrap;
+}
+.adminlog .logcol_msg {
+	width: 100%;
+}
+
+.adminlog .logcol_missed {
+	text-align: center;
+	border-width: 1px 0px;
+	border-style: dashed;
+	border-color: orange;
+}
+
+.adminlog .tracebtn {
+	cursor: pointer;
+}
+.adminlog .tracebtn:after {
+	content: "Show trace...";
+}
+.adminlog .tracebtn.visible:after {
+	content: "Hide trace...";
+}
+
+.adminlog .trace {
+	display: none;
+}
+
+.adminlog .trace.visible {
+	display: block;
+}
+
+.adminlog .trace span {
+	display: block;
+	margin-left: 30px;
+	text-indent: -30px;
+}
+
+.adminlog #markasread {
+	cursor: pointer;
+	border-radius: 5px;
+	background-color: #444;
+	color: orangered;
+	display: inline-block;
+	margin-top: 10px;
+	padding: 3px 5px 3px 5px;
+}
+
+
Index: /binary-improvements/webserver/index.html
===================================================================
--- /binary-improvements/webserver/index.html	(revision 249)
+++ /binary-improvements/webserver/index.html	(revision 250)
@@ -46,4 +46,5 @@
 	<script type="text/javascript" src="js/permissions.js"></script>
 	<script type="text/javascript" src="js/map.js"></script>
+	<script type="text/javascript" src="js/log.js"></script>
 
 	<!-- Own stylesheet -->
@@ -65,5 +66,5 @@
 				<ul>
 					<li><a href="#tab_map" data-permission="web.map">Map</a></li>
-					<li><a href="#tab_log" data-permission="web.log">Log</a></li>
+					<li><a href="#tab_log" data-permission="webapi.getlog">Log <span id="newlogcount"></span></a></li>
 				</ul>
 			</div>
@@ -85,8 +86,18 @@
 			</div>
 		</div>
-		<div class="admincontent">
+		<div id="admincontent">
 			<h1 id="nopermissionwarning" style="display:none">An error occured or you do not have any permissions on this WebPanel. Log in with the link on the lower left!</h1>
 			<div id="tab_map" class="adminmap"></div>
-			<div id="tab_log" class="adminlog"></div>
+			<div id="tab_log" class="adminlog">
+				<table>
+					<tr>
+						<th>Date/Time</th>
+						<th>Uptime</th>
+						<th>Severity</th>
+						<th>Message</th>
+					</tr>
+				</table>
+				<a id="markasread">Mark as read</a>
+			</div>
 		</div>
 	</div>
Index: /binary-improvements/webserver/js/index.js
===================================================================
--- /binary-improvements/webserver/js/index.js	(revision 249)
+++ /binary-improvements/webserver/js/index.js	(revision 250)
@@ -1,3 +1,6 @@
-InitializeTabs ();
+//InitializeTabs ();
+var tabs = $("#adminmenu").tabbedContent ({
+	contentdiv: $("#admincontent"),
+});
 SetupInventoryDialog ();
 InitPermissions ();
Index: /binary-improvements/webserver/js/inventory_dialog.js
===================================================================
--- /binary-improvements/webserver/js/inventory_dialog.js	(revision 249)
+++ /binary-improvements/webserver/js/inventory_dialog.js	(revision 250)
@@ -11,12 +11,22 @@
 		var cell = $("#" + containerTypeName + "Field"+cellIdent);
 		var text = $("#" + containerTypeName + "FieldText"+cellIdent);
-		if (itemdata.count > 0) {
+		var qual = $("#" + containerTypeName + "FieldQuality"+cellIdent);
+
+		cell.attr("style", "background-image: none;");
+		cell.removeAttr("title");
+		text.removeClass ("visible");
+		qual.removeClass ("visible");
+
+		if (itemdata !== null) {
 			cell.attr("style", "background-image: url(" + ITEMICONBASEURL + itemdata.name + ".png);");
-			cell.attr("title", itemdata.name);
-			text.text(itemdata.count);
-		} else {
-			cell.attr("style", "background-image: none;");
-			cell.removeAttr("title");
-			text.text("");
+			if (itemdata.quality >= 0) {
+				cell.attr("title", itemdata.name + " (quality: " + itemdata.quality + ")");
+				qual.attr("style", "background-color: #"+ itemdata.qualitycolor);
+				qual.addClass ("visible");
+			} else {
+				cell.attr("title", itemdata.name);
+				text.text(itemdata.count);
+				text.addClass ("visible");
+			}
 		}
 	}
@@ -24,7 +34,7 @@
 	var SetEquipmentItem = function (data, name, cellIdent) {
 		if (data.equipment [name] == false) {
-			SetCellItem ("equipment", cellIdent, { count: 0, name: "" });
+			SetCellItem ("equipment", cellIdent, null);
 		} else {
-			SetCellItem ("equipment", cellIdent, { count: 1, name: data.equipment [name] });
+			SetCellItem ("equipment", cellIdent, data.equipment [name] );
 		}
 	}
@@ -77,4 +87,5 @@
 	var CreateInvCell = function (containerTypeName, cellIdent) {
 		return "<td class=\"invField\" id=\"" + containerTypeName + "Field"+cellIdent+"\">" +
+			"<div class=\"invFieldQuality\" id=\"" + containerTypeName + "FieldQuality" + cellIdent + "\"></div>" +
 			"<span class=\"invFieldText\" id=\"" + containerTypeName + "FieldText"+cellIdent+"\"></span>" +
 			"</td>";
Index: /binary-improvements/webserver/js/log.js
===================================================================
--- /binary-improvements/webserver/js/log.js	(revision 250)
+++ /binary-improvements/webserver/js/log.js	(revision 250)
@@ -0,0 +1,79 @@
+var lastLogLine = -1;
+
+function StartLogModule () {
+	var maxLinesPerRequest = 50;
+
+	var timeout = null;
+	var table = $("#tab_log > table");
+	var lastRead = -1;
+	
+	
+	var updateEvent = function() {
+		$.getJSON( "../api/getlog?firstLine=" + (lastLogLine + 1) + "&lastLine=" + (lastLogLine + maxLinesPerRequest) )
+		.done(function(data) {
+			if (data.firstLine - lastLogLine - 1 > 0) {
+				var row = $("<tr></tr>").appendTo (table);
+				$('<td colspan="4">Missed ' + (data.firstLine - lastLogLine - 1) + ' log entries</td>').addClass ("logcol_missed").appendTo (row);
+			}
+			for (var i = 0; i < data.entries.length; i++) {
+				var row = $("<tr></tr>").addClass (data.entries [i].type).attr ("id", "line" + (data.firstLine + i)).appendTo (table);
+				$("<td>" + data.entries [i].date + " " + data.entries [i].time + "</td>").addClass ("logcol_datetime").appendTo (row);
+				$("<td>" + data.entries [i].uptime + "</td>").addClass ("logcol_uptime").appendTo (row);
+				$("<td>" + data.entries [i].type + "</td>").addClass ("logcol_type").appendTo (row);
+				var msg = $("<td>" + data.entries [i].msg + "</td>").addClass ("logcol_msg").appendTo (row);
+				if (data.entries [i].trace.length > 0) {
+					msg.append ('<br><div class="trace"><span>' + data.entries [i].trace.replace (/\n/g, "</span><span>") + '</span></div><a class="tracebtn"></a>');
+				}
+			}
+
+			if (data.entries.length > 0) {
+				lastLogLine = data.lastLine;
+			}
+		})
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching log lines");
+		})
+		.always(function() {
+		});
+		timeout = window.setTimeout(updateEvent, 2000);
+	};
+	
+	var markAsRead = function() {
+		lastRead = lastLogLine;
+		table.find (".readmark").removeClass ("readmark");
+		table.find ("#line" + (lastRead)).addClass ("readmark");
+	};
+	
+	table.on ("click.action", ".tracebtn", function (event) {
+		$(this).toggleClass ("visible");
+		$(this).prev ().toggleClass ("visible");
+	});
+	
+	$(".adminlog #markasread").on ("click.action", null, function (event) {
+		markAsRead ();
+	});
+	
+	
+	tabs.on ("tabbedcontenttabopened", function (event, data) {
+		if (data.newTab === "#tab_log") {
+			updateEvent ();
+
+			markAsRead ();
+			var markedrow = $(".adminlog .readmark");
+			if (markedrow.length > 0) {
+				window.setTimeout (function () {
+					$('html, body').scrollTop (markedrow.offset ().top);
+				}, 20);
+			}
+		} else {
+			window.clearTimeout (timeout);
+		}
+	});
+	
+	if (tabs.tabbedContent ("isTabOpen", "tab_log")) {
+		updateEvent ();
+	}
+
+}
+
+
Index: /binary-improvements/webserver/js/map.js
===================================================================
--- /binary-improvements/webserver/js/map.js	(revision 249)
+++ /binary-improvements/webserver/js/map.js	(revision 250)
@@ -131,7 +131,23 @@
 	});
 
+	var openedPopup = null;
+	var updatingMarkers = false;
+
+	map.on ("popupopen", function (event) {
+		console.log ("open");
+		console.log (event.popup._source);
+		openedPopup = event.popup._source;
+	});
+	map.on ("popupclose", function (event) {
+		if (!updatingMarkers) {
+			console.log ("close");
+			openedPopup = null;
+		}
+	});
+
 	var setPlayerMarkers = function(data) {
 		var online = 0;
 		var offline = 0;
+		updatingMarkers = true;
 		$.each( data, function( key, val ) {
 			var marker;
@@ -147,16 +163,20 @@
 				playersMappingList[val.steamid] = { online: !val.online };
 			}
-			if (playersMappingList[val.steamid].online) {
-				playersOnlineMarkerGroup.removeLayer(marker);
-			} else {
-				playersOfflineMarkerGroup.removeLayer(marker);
-			}
-			marker.setLatLng([val.position.x, val.position.z]);
-			if (val.online) {
-					marker.setOpacity(1.0);
-					playersOnlineMarkerGroup.addLayer(marker);
-			} else {
-					marker.setOpacity(0.5);
-					playersOfflineMarkerGroup.addLayer(marker);
+			
+			oldpos = marker.getLatLng ();
+			if ( playersMappingList[val.steamid].online != val.online || oldpos.lat != val.position.x || oldpos.lng != val.position.z ) {
+				if (playersMappingList[val.steamid].online) {
+					playersOnlineMarkerGroup.removeLayer(marker);
+				} else {
+					playersOfflineMarkerGroup.removeLayer(marker);
+				}
+				marker.setLatLng([val.position.x, val.position.z]);
+				if (val.online) {
+						marker.setOpacity(1.0);
+						playersOnlineMarkerGroup.addLayer(marker);
+				} else {
+						marker.setOpacity(0.5);
+						playersOfflineMarkerGroup.addLayer(marker);
+				}
 			}
 
@@ -169,8 +189,13 @@
 				offline++;
 		});
+		updatingMarkers = false;
+		if (openedPopup != null) {
+			openedPopup.openPopup ();
+		}
 		$( "#mapControlOnlineCount" ).text( online );
 		$( "#mapControlOfflineCount" ).text( offline );
 	}
 
+	var updatePlayerTimeout;
 	var updatePlayerEvent = function() {
 		$.getJSON( "../api/getplayerslocation")
@@ -180,10 +205,22 @@
 		})
 		.always(function() {
-			window.setTimeout(updatePlayerEvent, 2000);
+			updatePlayerTimeout = window.setTimeout(updatePlayerEvent, 4000);
 		});
 	}
 
-	if (HasPermission ("webapi.getplayerslocation")) {
-		window.setTimeout(updatePlayerEvent, 0);
+	tabs.on ("tabbedcontenttabopened", function (event, data) {
+		if (data.newTab === "#tab_map") {
+			if (HasPermission ("webapi.getplayerslocation")) {
+				updatePlayerEvent ();
+			}
+		} else {
+			window.clearTimeout (updatePlayerTimeout);
+		}
+	});
+	
+	if (tabs.tabbedContent ("isTabOpen", "tab_map")) {
+		if (HasPermission ("webapi.getplayerslocation")) {
+			updatePlayerEvent ();
+		}
 	}
 
Index: /binary-improvements/webserver/js/permissions.js
===================================================================
--- /binary-improvements/webserver/js/permissions.js	(revision 249)
+++ /binary-improvements/webserver/js/permissions.js	(revision 250)
@@ -21,13 +21,18 @@
 		}
 		
-		ApplyTabPermissions ();
-
 		if (HasPermission ("web.map")) {
 			StartMapModule ();
 		}		
+		if (HasPermission ("webapi.getlog")) {
+			StartLogModule ();
+		}
 		
-		if (HasPermission ("webapi.getstats")) {
+		if (HasPermission ("webapi.getwebuiupdates")) {
+			StartUIUpdatesModule ();
+		} else if (HasPermission ("webapi.getstats")) {
 			StartStatsModule ();
 		}
+
+		tabs.tabbedContent ("applyPermissions");
 
 	})
Index: /binary-improvements/webserver/js/stats.js
===================================================================
--- /binary-improvements/webserver/js/stats.js	(revision 249)
+++ /binary-improvements/webserver/js/stats.js	(revision 250)
@@ -25,2 +25,34 @@
 }
 
+function StartUIUpdatesModule () {
+	var updateGameTimeEvent = function() {
+		$.getJSON( "../api/getwebuiupdates?latestLine=" + lastLogLine)
+		.done(function(data) {
+			var time = "Day " + data.gametime.days + ", ";
+			if (data.gametime.hours < 10)
+				time += "0";
+			time += data.gametime.hours;
+			time += ":";
+			if (data.gametime.minutes < 10)
+				time += "0";
+			time += data.gametime.minutes;
+
+			$("#stats_time").html (time);
+			$("#stats_players").html (data.players);
+			$("#newlogcount").html (data.newlogs);
+			if (data.newlogs > 0) {
+				$("#newlogcount").addClass ("visible");
+			} else {
+				$("#newlogcount").removeClass ("visible");
+			}
+		})
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching ui updates");
+		})
+		.always(function() {
+		});
+		window.setTimeout(updateGameTimeEvent, 2000);
+	};
+	updateGameTimeEvent();
+}
+
Index: /binary-improvements/webserver/js/tabs.js
===================================================================
--- /binary-improvements/webserver/js/tabs.js	(revision 249)
+++ /binary-improvements/webserver/js/tabs.js	(revision 250)
@@ -1,40 +1,94 @@
-var tabElements = {};
-var currentTabClass = "current_tab";
+$.widget( "7dtd.tabbedContent", {
+	options: {
+		contentdiv: null,
+		currentTabClass: "current_tab",
+		menuButtonClass: "menu_button",
+		allowedMenuButtonClass: "allowed",
+		contentDivClass: "contenttab",
+	},
+	
+	_create: function () {
+		var options = this.options;
+		var self = this;
+		
+		if (options.contentdiv == null) {
+			console.log ("contentdiv has to be set!");
+		}
+		
+		this.element.find ("ul > li").addClass (options.menuButtonClass);
 
-function OpenTab () {
-	var menuElement = $(this);
-	var linkElement = menuElement.children ("a");
-	var linkName = linkElement.attr ("href");
+		options.contentdiv.children ("div").addClass (options.contentDivClass);
+		this.element.on ('click.action', "ul > li", function (event) {
+			var menuElement = $(this);
+			var linkElement = menuElement.children ("a");
+			var linkName = linkElement.attr ("href");
+			self.openTab (linkName);
+		});
 
-	$("*").removeClass (currentTabClass);
-	menuElement.addClass (currentTabClass);
-	$(linkName).addClass (currentTabClass);
-}
+		self.tabs = {};
+		this.element.find (".menu_button").each (function () {
+			self.tabs [$(this).children ("a").attr ("href")] = $(this);
+		});
+	},
+	
+	applyPermissions: function () {
+		var self = this;
+		this.element.find (".menu_button").each (function () {
+			if ($(this).children ("a").data ("permission")) {
+				var perm = $(this).children ("a").data ("permission");
+				if (HasPermission (perm)) {
+					$(this).addClass (self.options.allowedMenuButtonClass);
+				}
+			} else {
+				$(this).addClass (self.options.allowedMenuButtonClass);
+			}
+		});
 
-function InitializeTabs () {
-	$("#adminmenu > ul > li").addClass ("menu_button");
-	$(".admincontent > div").addClass ("contenttab");
-	$(".menu_button").on ('click.action', null, function (event) {
-		var menuElement = $(this);
-		var linkElement = menuElement.children ("a");
-		var linkName = linkElement.attr ("href");
+		this.element.find ("." + self.options.allowedMenuButtonClass).first ().click ();
+	},
+	
+	openTab: function (name) {
+		if (name.indexOf ("#") != 0)
+			name = "#" + name;
 		
-		$("*").removeClass ("current_tab");
-		menuElement.addClass ("current_tab");
-		$(linkName).addClass ("current_tab");
-	});
-}
+		if (!this.tabs.hasOwnProperty(name)) {
+			console.log ("no tab named " + name + " in " + this);
+			return;
+		}
 
-function ApplyTabPermissions () {
-	$("#adminmenu .menu_button").each (function () {
-		if ($(this).children ("a").data ("permission")) {
-			var perm = $(this).children ("a").data ("permission");
-			if (HasPermission (perm)) {
-				$(this).addClass ("allowed");
-			}
+		var menuElement = $(".menu_button > a[href=" + name + "]").parent ();
+
+		$("*").removeClass (this.options.currentTabClass);
+		menuElement.addClass (this.options.currentTabClass);
+		$(name).addClass (this.options.currentTabClass);
+		var oldTab = this.currentTab;
+		this.currentTab = name;
+	
+		if (oldTab != name) {
+			this._trigger ("tabopened", null, { oldTab: oldTab, newTab: name } );
 		}
-	});
+	},
+	
+	currentOpenTab: function () {
+		return this.currentTab;
+	},
+	
+	isTabOpen: function (name) {
+		if (name.indexOf ("#") != 0)
+			name = "#" + name;
 
-	$("#adminmenu .allowed").first ().click ();
-}
+		return this.currentTab == name;
+	},
 
+/*
+	value: function (value) {
+		if ( value === undefined ) {
+			return this.options.value;
+		} else {
+			this.options.value = this._constrain( value );
+			var progress = this.options.value + "%";
+			this.element.text( progress );
+		}
+	},
+*/
+});
