diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/webapp/dist/main.js b/mattermost/webapp/dist/main.js
index 8a0c4f1..4cca171 100644
--- a/mattermost/webapp/dist/main.js
+++ b/mattermost/webapp/dist/main.js
@@ -94,7 +94,121 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
-eval("\n\n//# sourceURL=webpack:///./src/index.jsx?");
+
+
+var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+class BathroomComponent extends _react.default.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      doors: {}
+    };
+  }
+
+  componentDidMount() {
+    Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+    this.updateDoors();
+  }
+
+  componentWillUnmount() {
+    Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+  }
+
+  async updateDoors(msg) {
+    var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+    var res = await fetch(url);
+    var json = await res.json();
+    this.setState({
+      "doors": json
+    });
+  }
+
+  render() {
+    if (!this.state.doors) return;
+    var columns = 6;
+    var keys = Object.keys(this.state.doors).sort();
+    var width = Math.floor(100 / columns) + "%";
+    var widthPx = "23px";
+    var elems = [];
+    var row = [];
+
+    for (var i = 0; i < keys.length
+    /*(Math.floor(keys.length / 6) + 1) * 6*/
+    ; i++) {
+      var img = null;
+
+      if (i < keys.length) {
+        var door = this.state.doors[keys[i]];
+        var imgFile = null;
+
+        switch (door) {
+          default:
+          case "unknown":
+            imgFile = "/static/emoji/2753.png";
+            break;
+
+          case "open":
+            imgFile = "/static/emoji/1f6bd.png";
+            break;
+
+          case "closed":
+            imgFile = "/static/emoji/26d4-fe0f.png";
+            break;
+        }
+
+        img = _react.default.createElement("img", {
+          src: imgFile,
+          width: "100%",
+          style: {
+            "padding": "2px"
+          }
+        });
+      }
+
+      row.push(_react.default.createElement("td", {
+        width: widthPx
+      }, img));
+
+      if (i % columns == columns - 1 || i == keys.length - 1) {
+        elems.push(_react.default.createElement(_react.default.Fragment, null, [...row]));
+        row.length = 0;
+      }
+    }
+
+    return (//
+      _react.default.createElement("div", {
+        style: {
+          "text-align": "center",
+          width: "100%",
+          "padding-top": "10px"
+        }
+      }, _react.default.createElement("table", {
+        style: {
+          "margin-left": "auto",
+          "margin-right": "auto"
+        }
+      }, elems))
+    );
+  }
+
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin {
+  initialize(registry, store) {
+    Registry = registry;
+    Store = store;
+    registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+  }
+
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
 
 /***/ }),
 
@@ -105,8 +219,21 @@
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 
-eval("module.exports = __webpack_require__(/*! ./src/index.jsx */\"./src/index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./src/index.jsx?");
+module.exports = __webpack_require__(/*! ./src/index.jsx */"./src/index.jsx");
+
+
+/***/ }),
+
+/***/ "react":
+/*!************************!*\
+  !*** external "React" ***!
+  \************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+module.exports = React;
 
 /***/ })
 
-/******/ });
\ No newline at end of file
+/******/ });
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/webapp/dist/main.js b/mattermost/webapp/dist/main.js
index 8a0c4f1..4cca171 100644
--- a/mattermost/webapp/dist/main.js
+++ b/mattermost/webapp/dist/main.js
@@ -94,7 +94,121 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
-eval("\n\n//# sourceURL=webpack:///./src/index.jsx?");
+
+
+var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+class BathroomComponent extends _react.default.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      doors: {}
+    };
+  }
+
+  componentDidMount() {
+    Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+    this.updateDoors();
+  }
+
+  componentWillUnmount() {
+    Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+  }
+
+  async updateDoors(msg) {
+    var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+    var res = await fetch(url);
+    var json = await res.json();
+    this.setState({
+      "doors": json
+    });
+  }
+
+  render() {
+    if (!this.state.doors) return;
+    var columns = 6;
+    var keys = Object.keys(this.state.doors).sort();
+    var width = Math.floor(100 / columns) + "%";
+    var widthPx = "23px";
+    var elems = [];
+    var row = [];
+
+    for (var i = 0; i < keys.length
+    /*(Math.floor(keys.length / 6) + 1) * 6*/
+    ; i++) {
+      var img = null;
+
+      if (i < keys.length) {
+        var door = this.state.doors[keys[i]];
+        var imgFile = null;
+
+        switch (door) {
+          default:
+          case "unknown":
+            imgFile = "/static/emoji/2753.png";
+            break;
+
+          case "open":
+            imgFile = "/static/emoji/1f6bd.png";
+            break;
+
+          case "closed":
+            imgFile = "/static/emoji/26d4-fe0f.png";
+            break;
+        }
+
+        img = _react.default.createElement("img", {
+          src: imgFile,
+          width: "100%",
+          style: {
+            "padding": "2px"
+          }
+        });
+      }
+
+      row.push(_react.default.createElement("td", {
+        width: widthPx
+      }, img));
+
+      if (i % columns == columns - 1 || i == keys.length - 1) {
+        elems.push(_react.default.createElement(_react.default.Fragment, null, [...row]));
+        row.length = 0;
+      }
+    }
+
+    return (//
+      _react.default.createElement("div", {
+        style: {
+          "text-align": "center",
+          width: "100%",
+          "padding-top": "10px"
+        }
+      }, _react.default.createElement("table", {
+        style: {
+          "margin-left": "auto",
+          "margin-right": "auto"
+        }
+      }, elems))
+    );
+  }
+
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin {
+  initialize(registry, store) {
+    Registry = registry;
+    Store = store;
+    registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+  }
+
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
 
 /***/ }),
 
@@ -105,8 +219,21 @@
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 
-eval("module.exports = __webpack_require__(/*! ./src/index.jsx */\"./src/index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./src/index.jsx?");
+module.exports = __webpack_require__(/*! ./src/index.jsx */"./src/index.jsx");
+
+
+/***/ }),
+
+/***/ "react":
+/*!************************!*\
+  !*** external "React" ***!
+  \************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+module.exports = React;
 
 /***/ })
 
-/******/ });
\ No newline at end of file
+/******/ });
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/mattermost/webapp/dist/main.js.map b/mattermost/webapp/dist/main.js.map
new file mode 100644
index 0000000..26114e4
--- /dev/null
+++ b/mattermost/webapp/dist/main.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index.jsx","webpack:///external \"React\""],"names":["BathroomComponent","React","Component","constructor","props","state","doors","componentDidMount","Registry","registerWebSocketEventHandler","updateDoors","bind","componentWillUnmount","unregisterWebSocketEventHandler","msg","url","Store","getState","entities","general","config","SiteURL","res","fetch","json","setState","render","columns","keys","Object","sort","width","Math","floor","widthPx","elems","row","i","length","img","door","imgFile","push","BathroomMonitorPlugin","initialize","registry","store","registerLeftSidebarHeaderComponent","window","registerPlugin"],"mappings":"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","file":"main.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","import React from 'react';\n\nclass BathroomComponent extends React.Component\n{\n    constructor(props)\n    {\n        super(props);\n        this.state = {doors:{}};\n    }\n    componentDidMount()\n    {\n        Registry.registerWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\", this.updateDoors.bind(this));\n        this.updateDoors();\n    }\n    componentWillUnmount()\n    {\n        Registry.unregisterWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\");\n    }\n    async updateDoors(msg)\n    {\n        var url = Store.getState().entities.general.config.SiteURL + \"/plugins/com.mattermost.bathroom/status\";\n        var res = await fetch(url);\n        var json = await res.json();\n        this.setState({\"doors\":json});\n    }\n    render()\n    {\n        if (!this.state.doors) return;\n        var columns = 6;\n        var keys = Object.keys(this.state.doors).sort();\n        var width = (Math.floor(100 / columns)) + \"%\";\n        var widthPx = \"23px\";\n        var elems = [];\n        var row = [];\n        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)\n        {\n            var img = null;\n            if (i < keys.length)\n            {\n                var door = this.state.doors[keys[i]];\n                var imgFile = null;\n                switch (door)\n                {\n                    default:\n                    case \"unknown\":\n                        imgFile = \"/static/emoji/2753.png\";\n                        break;\n                    case \"open\":\n                        imgFile = \"/static/emoji/1f6bd.png\";\n                        break;\n                    case \"closed\":\n                        imgFile = \"/static/emoji/26d4-fe0f.png\";\n                        break;\n                }\n                img = 
\n            }\n            row.push({img} | );\n            if (i % columns == columns - 1 || i == keys.length - 1)\n            {\n                elems.push({[...row]});\n                row.length = 0;\n            }\n        }\n        return (\n            //\n            \n        );\n    }\n}\n\nvar Registry = null;\nvar Store = null;\n\nclass BathroomMonitorPlugin\n{\n    initialize(registry, store)\n    {\n        Registry = registry;\n        Store = store;\n        registry.registerLeftSidebarHeaderComponent(BathroomComponent);\n    }\n}\n\nwindow.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());\n","module.exports = React;"],"sourceRoot":""}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/webapp/dist/main.js b/mattermost/webapp/dist/main.js
index 8a0c4f1..4cca171 100644
--- a/mattermost/webapp/dist/main.js
+++ b/mattermost/webapp/dist/main.js
@@ -94,7 +94,121 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
-eval("\n\n//# sourceURL=webpack:///./src/index.jsx?");
+
+
+var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+class BathroomComponent extends _react.default.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      doors: {}
+    };
+  }
+
+  componentDidMount() {
+    Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+    this.updateDoors();
+  }
+
+  componentWillUnmount() {
+    Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+  }
+
+  async updateDoors(msg) {
+    var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+    var res = await fetch(url);
+    var json = await res.json();
+    this.setState({
+      "doors": json
+    });
+  }
+
+  render() {
+    if (!this.state.doors) return;
+    var columns = 6;
+    var keys = Object.keys(this.state.doors).sort();
+    var width = Math.floor(100 / columns) + "%";
+    var widthPx = "23px";
+    var elems = [];
+    var row = [];
+
+    for (var i = 0; i < keys.length
+    /*(Math.floor(keys.length / 6) + 1) * 6*/
+    ; i++) {
+      var img = null;
+
+      if (i < keys.length) {
+        var door = this.state.doors[keys[i]];
+        var imgFile = null;
+
+        switch (door) {
+          default:
+          case "unknown":
+            imgFile = "/static/emoji/2753.png";
+            break;
+
+          case "open":
+            imgFile = "/static/emoji/1f6bd.png";
+            break;
+
+          case "closed":
+            imgFile = "/static/emoji/26d4-fe0f.png";
+            break;
+        }
+
+        img = _react.default.createElement("img", {
+          src: imgFile,
+          width: "100%",
+          style: {
+            "padding": "2px"
+          }
+        });
+      }
+
+      row.push(_react.default.createElement("td", {
+        width: widthPx
+      }, img));
+
+      if (i % columns == columns - 1 || i == keys.length - 1) {
+        elems.push(_react.default.createElement(_react.default.Fragment, null, [...row]));
+        row.length = 0;
+      }
+    }
+
+    return (//
+      _react.default.createElement("div", {
+        style: {
+          "text-align": "center",
+          width: "100%",
+          "padding-top": "10px"
+        }
+      }, _react.default.createElement("table", {
+        style: {
+          "margin-left": "auto",
+          "margin-right": "auto"
+        }
+      }, elems))
+    );
+  }
+
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin {
+  initialize(registry, store) {
+    Registry = registry;
+    Store = store;
+    registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+  }
+
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
 
 /***/ }),
 
@@ -105,8 +219,21 @@
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 
-eval("module.exports = __webpack_require__(/*! ./src/index.jsx */\"./src/index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./src/index.jsx?");
+module.exports = __webpack_require__(/*! ./src/index.jsx */"./src/index.jsx");
+
+
+/***/ }),
+
+/***/ "react":
+/*!************************!*\
+  !*** external "React" ***!
+  \************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+module.exports = React;
 
 /***/ })
 
-/******/ });
\ No newline at end of file
+/******/ });
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/mattermost/webapp/dist/main.js.map b/mattermost/webapp/dist/main.js.map
new file mode 100644
index 0000000..26114e4
--- /dev/null
+++ b/mattermost/webapp/dist/main.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index.jsx","webpack:///external \"React\""],"names":["BathroomComponent","React","Component","constructor","props","state","doors","componentDidMount","Registry","registerWebSocketEventHandler","updateDoors","bind","componentWillUnmount","unregisterWebSocketEventHandler","msg","url","Store","getState","entities","general","config","SiteURL","res","fetch","json","setState","render","columns","keys","Object","sort","width","Math","floor","widthPx","elems","row","i","length","img","door","imgFile","push","BathroomMonitorPlugin","initialize","registry","store","registerLeftSidebarHeaderComponent","window","registerPlugin"],"mappings":"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","file":"main.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","import React from 'react';\n\nclass BathroomComponent extends React.Component\n{\n    constructor(props)\n    {\n        super(props);\n        this.state = {doors:{}};\n    }\n    componentDidMount()\n    {\n        Registry.registerWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\", this.updateDoors.bind(this));\n        this.updateDoors();\n    }\n    componentWillUnmount()\n    {\n        Registry.unregisterWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\");\n    }\n    async updateDoors(msg)\n    {\n        var url = Store.getState().entities.general.config.SiteURL + \"/plugins/com.mattermost.bathroom/status\";\n        var res = await fetch(url);\n        var json = await res.json();\n        this.setState({\"doors\":json});\n    }\n    render()\n    {\n        if (!this.state.doors) return;\n        var columns = 6;\n        var keys = Object.keys(this.state.doors).sort();\n        var width = (Math.floor(100 / columns)) + \"%\";\n        var widthPx = \"23px\";\n        var elems = [];\n        var row = [];\n        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)\n        {\n            var img = null;\n            if (i < keys.length)\n            {\n                var door = this.state.doors[keys[i]];\n                var imgFile = null;\n                switch (door)\n                {\n                    default:\n                    case \"unknown\":\n                        imgFile = \"/static/emoji/2753.png\";\n                        break;\n                    case \"open\":\n                        imgFile = \"/static/emoji/1f6bd.png\";\n                        break;\n                    case \"closed\":\n                        imgFile = \"/static/emoji/26d4-fe0f.png\";\n                        break;\n                }\n                img = 
\n            }\n            row.push({img} | );\n            if (i % columns == columns - 1 || i == keys.length - 1)\n            {\n                elems.push({[...row]});\n                row.length = 0;\n            }\n        }\n        return (\n            //\n            \n        );\n    }\n}\n\nvar Registry = null;\nvar Store = null;\n\nclass BathroomMonitorPlugin\n{\n    initialize(registry, store)\n    {\n        Registry = registry;\n        Store = store;\n        registry.registerLeftSidebarHeaderComponent(BathroomComponent);\n    }\n}\n\nwindow.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());\n","module.exports = React;"],"sourceRoot":""}
\ No newline at end of file
diff --git a/mattermost/webapp/src/index.jsx b/mattermost/webapp/src/index.jsx
index e69de29..0868e33 100644
--- a/mattermost/webapp/src/index.jsx
+++ b/mattermost/webapp/src/index.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+
+class BathroomComponent extends React.Component
+{
+    constructor(props)
+    {
+        super(props);
+        this.state = {doors:{}};
+    }
+    componentDidMount()
+    {
+        Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+        this.updateDoors();
+    }
+    componentWillUnmount()
+    {
+        Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+    }
+    async updateDoors(msg)
+    {
+        var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+        var res = await fetch(url);
+        var json = await res.json();
+        this.setState({"doors":json});
+    }
+    render()
+    {
+        if (!this.state.doors) return;
+        var columns = 6;
+        var keys = Object.keys(this.state.doors).sort();
+        var width = (Math.floor(100 / columns)) + "%";
+        var widthPx = "23px";
+        var elems = [];
+        var row = [];
+        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)
+        {
+            var img = null;
+            if (i < keys.length)
+            {
+                var door = this.state.doors[keys[i]];
+                var imgFile = null;
+                switch (door)
+                {
+                    default:
+                    case "unknown":
+                        imgFile = "/static/emoji/2753.png";
+                        break;
+                    case "open":
+                        imgFile = "/static/emoji/1f6bd.png";
+                        break;
+                    case "closed":
+                        imgFile = "/static/emoji/26d4-fe0f.png";
+                        break;
+                }
+                img = 
+            }
+            row.push({img} | );
+            if (i % columns == columns - 1 || i == keys.length - 1)
+            {
+                elems.push({[...row]});
+                row.length = 0;
+            }
+        }
+        return (
+            //
+            
+        );
+    }
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin
+{
+    initialize(registry, store)
+    {
+        Registry = registry;
+        Store = store;
+        registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+    }
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/webapp/dist/main.js b/mattermost/webapp/dist/main.js
index 8a0c4f1..4cca171 100644
--- a/mattermost/webapp/dist/main.js
+++ b/mattermost/webapp/dist/main.js
@@ -94,7 +94,121 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
-eval("\n\n//# sourceURL=webpack:///./src/index.jsx?");
+
+
+var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+class BathroomComponent extends _react.default.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      doors: {}
+    };
+  }
+
+  componentDidMount() {
+    Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+    this.updateDoors();
+  }
+
+  componentWillUnmount() {
+    Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+  }
+
+  async updateDoors(msg) {
+    var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+    var res = await fetch(url);
+    var json = await res.json();
+    this.setState({
+      "doors": json
+    });
+  }
+
+  render() {
+    if (!this.state.doors) return;
+    var columns = 6;
+    var keys = Object.keys(this.state.doors).sort();
+    var width = Math.floor(100 / columns) + "%";
+    var widthPx = "23px";
+    var elems = [];
+    var row = [];
+
+    for (var i = 0; i < keys.length
+    /*(Math.floor(keys.length / 6) + 1) * 6*/
+    ; i++) {
+      var img = null;
+
+      if (i < keys.length) {
+        var door = this.state.doors[keys[i]];
+        var imgFile = null;
+
+        switch (door) {
+          default:
+          case "unknown":
+            imgFile = "/static/emoji/2753.png";
+            break;
+
+          case "open":
+            imgFile = "/static/emoji/1f6bd.png";
+            break;
+
+          case "closed":
+            imgFile = "/static/emoji/26d4-fe0f.png";
+            break;
+        }
+
+        img = _react.default.createElement("img", {
+          src: imgFile,
+          width: "100%",
+          style: {
+            "padding": "2px"
+          }
+        });
+      }
+
+      row.push(_react.default.createElement("td", {
+        width: widthPx
+      }, img));
+
+      if (i % columns == columns - 1 || i == keys.length - 1) {
+        elems.push(_react.default.createElement(_react.default.Fragment, null, [...row]));
+        row.length = 0;
+      }
+    }
+
+    return (//
+      _react.default.createElement("div", {
+        style: {
+          "text-align": "center",
+          width: "100%",
+          "padding-top": "10px"
+        }
+      }, _react.default.createElement("table", {
+        style: {
+          "margin-left": "auto",
+          "margin-right": "auto"
+        }
+      }, elems))
+    );
+  }
+
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin {
+  initialize(registry, store) {
+    Registry = registry;
+    Store = store;
+    registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+  }
+
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
 
 /***/ }),
 
@@ -105,8 +219,21 @@
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 
-eval("module.exports = __webpack_require__(/*! ./src/index.jsx */\"./src/index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./src/index.jsx?");
+module.exports = __webpack_require__(/*! ./src/index.jsx */"./src/index.jsx");
+
+
+/***/ }),
+
+/***/ "react":
+/*!************************!*\
+  !*** external "React" ***!
+  \************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+module.exports = React;
 
 /***/ })
 
-/******/ });
\ No newline at end of file
+/******/ });
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/mattermost/webapp/dist/main.js.map b/mattermost/webapp/dist/main.js.map
new file mode 100644
index 0000000..26114e4
--- /dev/null
+++ b/mattermost/webapp/dist/main.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index.jsx","webpack:///external \"React\""],"names":["BathroomComponent","React","Component","constructor","props","state","doors","componentDidMount","Registry","registerWebSocketEventHandler","updateDoors","bind","componentWillUnmount","unregisterWebSocketEventHandler","msg","url","Store","getState","entities","general","config","SiteURL","res","fetch","json","setState","render","columns","keys","Object","sort","width","Math","floor","widthPx","elems","row","i","length","img","door","imgFile","push","BathroomMonitorPlugin","initialize","registry","store","registerLeftSidebarHeaderComponent","window","registerPlugin"],"mappings":"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","file":"main.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","import React from 'react';\n\nclass BathroomComponent extends React.Component\n{\n    constructor(props)\n    {\n        super(props);\n        this.state = {doors:{}};\n    }\n    componentDidMount()\n    {\n        Registry.registerWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\", this.updateDoors.bind(this));\n        this.updateDoors();\n    }\n    componentWillUnmount()\n    {\n        Registry.unregisterWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\");\n    }\n    async updateDoors(msg)\n    {\n        var url = Store.getState().entities.general.config.SiteURL + \"/plugins/com.mattermost.bathroom/status\";\n        var res = await fetch(url);\n        var json = await res.json();\n        this.setState({\"doors\":json});\n    }\n    render()\n    {\n        if (!this.state.doors) return;\n        var columns = 6;\n        var keys = Object.keys(this.state.doors).sort();\n        var width = (Math.floor(100 / columns)) + \"%\";\n        var widthPx = \"23px\";\n        var elems = [];\n        var row = [];\n        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)\n        {\n            var img = null;\n            if (i < keys.length)\n            {\n                var door = this.state.doors[keys[i]];\n                var imgFile = null;\n                switch (door)\n                {\n                    default:\n                    case \"unknown\":\n                        imgFile = \"/static/emoji/2753.png\";\n                        break;\n                    case \"open\":\n                        imgFile = \"/static/emoji/1f6bd.png\";\n                        break;\n                    case \"closed\":\n                        imgFile = \"/static/emoji/26d4-fe0f.png\";\n                        break;\n                }\n                img = 
\n            }\n            row.push({img} | );\n            if (i % columns == columns - 1 || i == keys.length - 1)\n            {\n                elems.push({[...row]});\n                row.length = 0;\n            }\n        }\n        return (\n            //\n            \n        );\n    }\n}\n\nvar Registry = null;\nvar Store = null;\n\nclass BathroomMonitorPlugin\n{\n    initialize(registry, store)\n    {\n        Registry = registry;\n        Store = store;\n        registry.registerLeftSidebarHeaderComponent(BathroomComponent);\n    }\n}\n\nwindow.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());\n","module.exports = React;"],"sourceRoot":""}
\ No newline at end of file
diff --git a/mattermost/webapp/src/index.jsx b/mattermost/webapp/src/index.jsx
index e69de29..0868e33 100644
--- a/mattermost/webapp/src/index.jsx
+++ b/mattermost/webapp/src/index.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+
+class BathroomComponent extends React.Component
+{
+    constructor(props)
+    {
+        super(props);
+        this.state = {doors:{}};
+    }
+    componentDidMount()
+    {
+        Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+        this.updateDoors();
+    }
+    componentWillUnmount()
+    {
+        Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+    }
+    async updateDoors(msg)
+    {
+        var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+        var res = await fetch(url);
+        var json = await res.json();
+        this.setState({"doors":json});
+    }
+    render()
+    {
+        if (!this.state.doors) return;
+        var columns = 6;
+        var keys = Object.keys(this.state.doors).sort();
+        var width = (Math.floor(100 / columns)) + "%";
+        var widthPx = "23px";
+        var elems = [];
+        var row = [];
+        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)
+        {
+            var img = null;
+            if (i < keys.length)
+            {
+                var door = this.state.doors[keys[i]];
+                var imgFile = null;
+                switch (door)
+                {
+                    default:
+                    case "unknown":
+                        imgFile = "/static/emoji/2753.png";
+                        break;
+                    case "open":
+                        imgFile = "/static/emoji/1f6bd.png";
+                        break;
+                    case "closed":
+                        imgFile = "/static/emoji/26d4-fe0f.png";
+                        break;
+                }
+                img = 
+            }
+            row.push({img} | );
+            if (i % columns == columns - 1 || i == keys.length - 1)
+            {
+                elems.push({[...row]});
+                row.length = 0;
+            }
+        }
+        return (
+            //
+            
+        );
+    }
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin
+{
+    initialize(registry, store)
+    {
+        Registry = registry;
+        Store = store;
+        registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+    }
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
diff --git a/mattermost/webapp/webpack.config.js b/mattermost/webapp/webpack.config.js
index 57121e1..b7e2419 100644
--- a/mattermost/webapp/webpack.config.js
+++ b/mattermost/webapp/webpack.config.js
@@ -4,6 +4,7 @@
     entry: [
         './src/index.jsx',
     ],
+    devtool: 'source-map',
     resolve: {
         modules: [
             'src',
diff --git a/.gitignore b/.gitignore
index 1377554..3819313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.swp
+*.swo
diff --git a/mattermost/bathroom.tar.gz b/mattermost/bathroom.tar.gz
new file mode 100644
index 0000000..246e3c9
--- /dev/null
+++ b/mattermost/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/install_deps.sh b/mattermost/install_deps.sh
new file mode 100644
index 0000000..6a3cec5
--- /dev/null
+++ b/mattermost/install_deps.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+
+sudo apt-get install filepp
diff --git a/mattermost/package.sh b/mattermost/package.sh
new file mode 100644
index 0000000..7253322
--- /dev/null
+++ b/mattermost/package.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+tar -czvf bathroom.tar.gz plugin.json -C server bathroom-linux-amd64 -C ../webapp/dist main.js
diff --git a/mattermost/plugin.json b/mattermost/plugin.json
new file mode 100644
index 0000000..ba3a186
--- /dev/null
+++ b/mattermost/plugin.json
@@ -0,0 +1,17 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/plugin_templ.json b/mattermost/plugin_templ.json
new file mode 100644
index 0000000..634b204
--- /dev/null
+++ b/mattermost/plugin_templ.json
@@ -0,0 +1,21 @@
+{
+    "id": "com.mattermost.bathroom",
+    "name": "Bathroom Monitor",
+    "server": {
+        "executable": "bathroom-linux-amd64"
+    },
+    "webapp": {
+	"bundle_path": "main.js"
+    },
+    "settings_schema": {
+	"settings": [
+		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
+#ifdef fsnotify
+		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#else
+		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
+#endif
+		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
+	]
+    }
+}
diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64
index 681e00f..c478ac9 100755
--- a/mattermost/server/bathroom-linux-amd64
+++ b/mattermost/server/bathroom-linux-amd64
Binary files differ
diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go
index 225a000..b3512ab 100644
--- a/mattermost/server/bathroom.go
+++ b/mattermost/server/bathroom.go
@@ -30,6 +30,8 @@
 	"encoding/base64"
 )
 
+const DO_LOGGING = false
+
 var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`)
 
 const (
@@ -139,6 +141,12 @@
 	}
 }
 
+func (p *BathroomMonitorPlugin) log(log string) {
+	if DO_LOGGING {
+		p.API.LogInfo(log)
+	}
+}
+
 func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error {
 	if id < 1 || id > p.config.numDoors {
 		return errors.New(fmt.Sprintf("Invalid door id %d", id))
@@ -156,8 +164,12 @@
 		p.doors[id - 1].status = status
 		if report {
 			statusStr, _ := statusName(status)
-			p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			_ = statusStr
+			//p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr))
+			p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{})
 		}
+	} else {
+		p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status))
 	}
 
 	return nil
@@ -186,12 +198,12 @@
 	p.configLock.Unlock()
 
 
-	doorVal, ok := r.Form["door_id"]
-	if !ok || len(doorVal) == 0 {
+	doorStr, ok := r.Form["door_id"]
+	if !ok || len(doorStr) == 0 {
 		return 0, errors.New("Please send door id")
 	}
 
-	doorId, err := strconv.ParseUint(doorVal[0], 10, 8)
+	doorId, err := strconv.ParseUint(doorStr[0], 10, 8)
 	if err != nil {
 		return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error()))
 	}
@@ -205,7 +217,7 @@
 }
 
 func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
-	p.API.LogInfo(fmt.Sprintf("Requested path: %s", r.URL.Path))
+	p.log(fmt.Sprintf("Requested path: %s %s", r.URL.Path, c.IpAddress))
 	if r.URL.Path == "/status" {
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
@@ -224,7 +236,7 @@
 	}
 	if r.URL.Path == "/status-update" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
@@ -232,13 +244,13 @@
 			return
 		}
 
-		statusVal, ok := r.Form["status"]
-		if !ok || len(statusVal) == 0 {
+		statusStr, ok := r.Form["status"]
+		if !ok || len(statusStr) == 0 {
 			fmt.Fprint(w, "Please send door status")
 			return
 		}
 
-		status, err := strconv.ParseUint(statusVal[0], 10, 8)
+		status, err := strconv.ParseUint(statusStr[0], 10, 8)
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't parse status: %s", err.Error())
 			return
@@ -249,6 +261,11 @@
 			return
 		}
 
+		statusVal := Open
+		if status == 1 {
+			statusVal = Closed
+		}
+
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
 
@@ -257,34 +274,34 @@
 			return
 		}
 
-		p.API.LogInfo("Getting random bytes")
+		p.log("Getting random bytes")
 		var verifyBytes [50]byte
 		rand.Read(verifyBytes[:])
-		p.API.LogInfo("Encoding random bytes")
+		p.log("Encoding random bytes")
 
 		verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:])
 
-		p.API.LogInfo("Encryping random bytes")
+		p.log("Encryping random bytes")
 		encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{})
 		if err != nil {
 			fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error())
 			return
 		}
 
-		p.API.LogInfo("Encoding encrypted bytes")
+		p.log("Encoding encrypted bytes")
 		encryptedB64 := base64.StdEncoding.EncodeToString(encrypted)
 
 
 		req := &DoorRequest {
-			ip:r.RemoteAddr,
+			ip:c.IpAddress,
 			time:time.Now().Unix(),
 			verify:verifyB64,
-			status: uint(status),
+			status: statusVal,
 		}
 
 		p.doors[doorId8 - 1].lastRequest = req
 
-		p.API.LogInfo(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
+		p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify))
 
 		fmt.Fprint(w, encryptedB64)
 
@@ -293,7 +310,7 @@
 	}
 	if r.URL.Path == "/verify-status" {
 		r.ParseForm()
-		p.API.LogInfo("Contacted by " + r.RemoteAddr)
+		p.log("Contacted by " + r.RemoteAddr)
 		doorId8, err := p.validateRequestDoorId(r)
 		if err != nil {
 			fmt.Fprint(w, err.Error())
@@ -306,8 +323,10 @@
 			return
 		}
 
+		p.log("Verify locking")
 		p.doorLock.Lock()
 		defer p.doorLock.Unlock()
+		p.log("Verify done locking")
 
 		if p.doors[doorId8 - 1].lastRequest == nil {
 			fmt.Fprint(w, "Invalid request")
@@ -315,7 +334,7 @@
 		}
 
 		req := p.doors[doorId8 - 1].lastRequest
-		if req.ip != r.RemoteAddr {
+		if req.ip != c.IpAddress {
 			fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr)
 			return
 		}
@@ -328,17 +347,20 @@
 
 
 		if req.verify != verifyB64[0] {
+			p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0]))
 			fmt.Fprint(w, "Unauthorized request")
 			return
 		}
 
 
-		p.doorLock.Lock()
-		defer p.doorLock.Unlock()
-
 		p.doors[doorId8 - 1].lastRequest = nil
 
-		p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changing")
+		err = p.setDoorStatus(doorId8, req.status, true)
+		p.log("Changed")
+		if err != nil {
+			fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error())
+		}
 
 		return
 
@@ -463,7 +485,7 @@
 
 	p.initDoors()
 
-	p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
+	p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath))
 
 	if configErr != nil {
 		p.postAdminChannel(configErr.Error())
@@ -562,7 +584,7 @@
 					}
 				}
 			case <- p.configChanged:
-				p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
+				p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates))
 				run = false
 			}
 		}
diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz
deleted file mode 100644
index f2298e9..0000000
--- a/mattermost/server/bathroom.tar.gz
+++ /dev/null
Binary files differ
diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh
index 916a360..c2c1a0e 100755
--- a/mattermost/server/build.sh
+++ b/mattermost/server/build.sh
@@ -1,4 +1,4 @@
 #! /usr/bin/env bash
 
-filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json
+filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json
 GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES
diff --git a/mattermost/server/package.sh b/mattermost/server/package.sh
deleted file mode 100755
index c8ce874..0000000
--- a/mattermost/server/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-
-tar -czvf bathroom.tar.gz bathroom-linux-amd64 plugin.json
diff --git a/mattermost/server/plugin.json b/mattermost/server/plugin.json
deleted file mode 100644
index e4d22cb..0000000
--- a/mattermost/server/plugin.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json
deleted file mode 100644
index 1de221a..0000000
--- a/mattermost/server/plugin_templ.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "com.mattermost.bathroom",
-    "name": "Bathroom Monitor",
-    "server": {
-        "executable": "bathroom-linux-amd64"
-    },
-    "settings_schema": {
-	"settings": [
-		{"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"},
-#ifdef fsnotify
-		{"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#else
-		{"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"},
-#endif
-		{"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}
-	]
-    }
-}
diff --git a/mattermost/webapp/bathroom.tar.gz b/mattermost/webapp/bathroom.tar.gz
new file mode 100644
index 0000000..48d8426
--- /dev/null
+++ b/mattermost/webapp/bathroom.tar.gz
Binary files differ
diff --git a/mattermost/webapp/dist/main.js b/mattermost/webapp/dist/main.js
index 8a0c4f1..4cca171 100644
--- a/mattermost/webapp/dist/main.js
+++ b/mattermost/webapp/dist/main.js
@@ -94,7 +94,121 @@
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
-eval("\n\n//# sourceURL=webpack:///./src/index.jsx?");
+
+
+var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+class BathroomComponent extends _react.default.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      doors: {}
+    };
+  }
+
+  componentDidMount() {
+    Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+    this.updateDoors();
+  }
+
+  componentWillUnmount() {
+    Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+  }
+
+  async updateDoors(msg) {
+    var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+    var res = await fetch(url);
+    var json = await res.json();
+    this.setState({
+      "doors": json
+    });
+  }
+
+  render() {
+    if (!this.state.doors) return;
+    var columns = 6;
+    var keys = Object.keys(this.state.doors).sort();
+    var width = Math.floor(100 / columns) + "%";
+    var widthPx = "23px";
+    var elems = [];
+    var row = [];
+
+    for (var i = 0; i < keys.length
+    /*(Math.floor(keys.length / 6) + 1) * 6*/
+    ; i++) {
+      var img = null;
+
+      if (i < keys.length) {
+        var door = this.state.doors[keys[i]];
+        var imgFile = null;
+
+        switch (door) {
+          default:
+          case "unknown":
+            imgFile = "/static/emoji/2753.png";
+            break;
+
+          case "open":
+            imgFile = "/static/emoji/1f6bd.png";
+            break;
+
+          case "closed":
+            imgFile = "/static/emoji/26d4-fe0f.png";
+            break;
+        }
+
+        img = _react.default.createElement("img", {
+          src: imgFile,
+          width: "100%",
+          style: {
+            "padding": "2px"
+          }
+        });
+      }
+
+      row.push(_react.default.createElement("td", {
+        width: widthPx
+      }, img));
+
+      if (i % columns == columns - 1 || i == keys.length - 1) {
+        elems.push(_react.default.createElement(_react.default.Fragment, null, [...row]));
+        row.length = 0;
+      }
+    }
+
+    return (//
+      _react.default.createElement("div", {
+        style: {
+          "text-align": "center",
+          width: "100%",
+          "padding-top": "10px"
+        }
+      }, _react.default.createElement("table", {
+        style: {
+          "margin-left": "auto",
+          "margin-right": "auto"
+        }
+      }, elems))
+    );
+  }
+
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin {
+  initialize(registry, store) {
+    Registry = registry;
+    Store = store;
+    registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+  }
+
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
 
 /***/ }),
 
@@ -105,8 +219,21 @@
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 
-eval("module.exports = __webpack_require__(/*! ./src/index.jsx */\"./src/index.jsx\");\n\n\n//# sourceURL=webpack:///multi_./src/index.jsx?");
+module.exports = __webpack_require__(/*! ./src/index.jsx */"./src/index.jsx");
+
+
+/***/ }),
+
+/***/ "react":
+/*!************************!*\
+  !*** external "React" ***!
+  \************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+module.exports = React;
 
 /***/ })
 
-/******/ });
\ No newline at end of file
+/******/ });
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/mattermost/webapp/dist/main.js.map b/mattermost/webapp/dist/main.js.map
new file mode 100644
index 0000000..26114e4
--- /dev/null
+++ b/mattermost/webapp/dist/main.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index.jsx","webpack:///external \"React\""],"names":["BathroomComponent","React","Component","constructor","props","state","doors","componentDidMount","Registry","registerWebSocketEventHandler","updateDoors","bind","componentWillUnmount","unregisterWebSocketEventHandler","msg","url","Store","getState","entities","general","config","SiteURL","res","fetch","json","setState","render","columns","keys","Object","sort","width","Math","floor","widthPx","elems","row","i","length","img","door","imgFile","push","BathroomMonitorPlugin","initialize","registry","store","registerLeftSidebarHeaderComponent","window","registerPlugin"],"mappings":"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","file":"main.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","import React from 'react';\n\nclass BathroomComponent extends React.Component\n{\n    constructor(props)\n    {\n        super(props);\n        this.state = {doors:{}};\n    }\n    componentDidMount()\n    {\n        Registry.registerWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\", this.updateDoors.bind(this));\n        this.updateDoors();\n    }\n    componentWillUnmount()\n    {\n        Registry.unregisterWebSocketEventHandler(\"custom_com.mattermost.bathroom_updated\");\n    }\n    async updateDoors(msg)\n    {\n        var url = Store.getState().entities.general.config.SiteURL + \"/plugins/com.mattermost.bathroom/status\";\n        var res = await fetch(url);\n        var json = await res.json();\n        this.setState({\"doors\":json});\n    }\n    render()\n    {\n        if (!this.state.doors) return;\n        var columns = 6;\n        var keys = Object.keys(this.state.doors).sort();\n        var width = (Math.floor(100 / columns)) + \"%\";\n        var widthPx = \"23px\";\n        var elems = [];\n        var row = [];\n        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)\n        {\n            var img = null;\n            if (i < keys.length)\n            {\n                var door = this.state.doors[keys[i]];\n                var imgFile = null;\n                switch (door)\n                {\n                    default:\n                    case \"unknown\":\n                        imgFile = \"/static/emoji/2753.png\";\n                        break;\n                    case \"open\":\n                        imgFile = \"/static/emoji/1f6bd.png\";\n                        break;\n                    case \"closed\":\n                        imgFile = \"/static/emoji/26d4-fe0f.png\";\n                        break;\n                }\n                img = 
\n            }\n            row.push({img} | );\n            if (i % columns == columns - 1 || i == keys.length - 1)\n            {\n                elems.push({[...row]});\n                row.length = 0;\n            }\n        }\n        return (\n            //\n            \n        );\n    }\n}\n\nvar Registry = null;\nvar Store = null;\n\nclass BathroomMonitorPlugin\n{\n    initialize(registry, store)\n    {\n        Registry = registry;\n        Store = store;\n        registry.registerLeftSidebarHeaderComponent(BathroomComponent);\n    }\n}\n\nwindow.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());\n","module.exports = React;"],"sourceRoot":""}
\ No newline at end of file
diff --git a/mattermost/webapp/src/index.jsx b/mattermost/webapp/src/index.jsx
index e69de29..0868e33 100644
--- a/mattermost/webapp/src/index.jsx
+++ b/mattermost/webapp/src/index.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+
+class BathroomComponent extends React.Component
+{
+    constructor(props)
+    {
+        super(props);
+        this.state = {doors:{}};
+    }
+    componentDidMount()
+    {
+        Registry.registerWebSocketEventHandler("custom_com.mattermost.bathroom_updated", this.updateDoors.bind(this));
+        this.updateDoors();
+    }
+    componentWillUnmount()
+    {
+        Registry.unregisterWebSocketEventHandler("custom_com.mattermost.bathroom_updated");
+    }
+    async updateDoors(msg)
+    {
+        var url = Store.getState().entities.general.config.SiteURL + "/plugins/com.mattermost.bathroom/status";
+        var res = await fetch(url);
+        var json = await res.json();
+        this.setState({"doors":json});
+    }
+    render()
+    {
+        if (!this.state.doors) return;
+        var columns = 6;
+        var keys = Object.keys(this.state.doors).sort();
+        var width = (Math.floor(100 / columns)) + "%";
+        var widthPx = "23px";
+        var elems = [];
+        var row = [];
+        for (var i = 0; i < keys.length /*(Math.floor(keys.length / 6) + 1) * 6*/; i++)
+        {
+            var img = null;
+            if (i < keys.length)
+            {
+                var door = this.state.doors[keys[i]];
+                var imgFile = null;
+                switch (door)
+                {
+                    default:
+                    case "unknown":
+                        imgFile = "/static/emoji/2753.png";
+                        break;
+                    case "open":
+                        imgFile = "/static/emoji/1f6bd.png";
+                        break;
+                    case "closed":
+                        imgFile = "/static/emoji/26d4-fe0f.png";
+                        break;
+                }
+                img = 
+            }
+            row.push({img} | );
+            if (i % columns == columns - 1 || i == keys.length - 1)
+            {
+                elems.push({[...row]});
+                row.length = 0;
+            }
+        }
+        return (
+            //
+            
+        );
+    }
+}
+
+var Registry = null;
+var Store = null;
+
+class BathroomMonitorPlugin
+{
+    initialize(registry, store)
+    {
+        Registry = registry;
+        Store = store;
+        registry.registerLeftSidebarHeaderComponent(BathroomComponent);
+    }
+}
+
+window.registerPlugin('com.mattermost.bathroom', new BathroomMonitorPlugin());
diff --git a/mattermost/webapp/webpack.config.js b/mattermost/webapp/webpack.config.js
index 57121e1..b7e2419 100644
--- a/mattermost/webapp/webpack.config.js
+++ b/mattermost/webapp/webpack.config.js
@@ -4,6 +4,7 @@
     entry: [
         './src/index.jsx',
     ],
+    devtool: 'source-map',
     resolve: {
         modules: [
             'src',
diff --git a/pi/monitor.py b/pi/monitor.py
index 5d37f82..18d9312 100644
--- a/pi/monitor.py
+++ b/pi/monitor.py
@@ -38,6 +38,32 @@
 private_key = RSA.import_key(open("private.pem").read())
 rsa = PKCS1_OAEP.new(private_key);
 
+def report(val):
+      try:
+          url = url_base + "status-update" + ext + "?door_id=" + str(id) + "&status=" + ("1" if val else "0")
+          print " open " + url
+          response = urllib2.urlopen(url)
+
+          responseStr = response.read()
+          print "Response: " + responseStr
+          enc_verify = base64.b64decode(responseStr)
+
+          verify = rsa.decrypt(enc_verify)
+
+          if ext == "":
+              verify = base64.b64encode(verify)
+
+          print " verifying with: " + verify
+
+          url = url_base + "verify-status" + ext + "?door_id=" + str(id) + "&verify=" + verify
+          print " open " + url
+          response = urllib2.urlopen(url)
+          print " returned: " + response.read()
+      except Exception as e:
+          print str(e)
+          traceback.print_tb(sys.exc_info()[2])
+
+report(False)
 while True:
     now_pressed = b.is_pressed
     if now_pressed == status:
@@ -48,31 +74,8 @@
         now_pressed = b.is_pressed
     if now_pressed != status:
         status = now_pressed
-
         print "status now: " + str(status)
 
-        try:
-            url = url_base + "status-update" + ext + "?door_id=" + str(id) + "&status=" + ("1" if status else "0")
-            print " open " + url
-            response = urllib2.urlopen(url)
-
-            responseStr = response.read()
-            print "Response: " + responseStr
-            enc_verify = base64.b64decode(responseStr)
-
-            verify = rsa.decrypt(enc_verify)
-
-            if ext == "":
-                verify = base64.b64encode(verify)
-
-            print " verifying with: " + verify
-
-            url = url_base + "verify-status" + ext + "?door_id=" + str(id) + "&verify=" + urllib.quote(verify)
-            print " open " + url
-            response = urllib2.urlopen(url)
-            print " returned: " + response.read()
-        except Exception as e:
-            print str(e)
-            traceback.print_tb(sys.exc_info()[2])
+        report(status)
 
         sleep(1)