diff --git a/data/favicon.ico b/data/favicon.ico
index 71b25fe6ee6012a4c26602977262d217af885520..e625327072d434ecf199703b86f1ac812a4030c2 100644
Binary files a/data/favicon.ico and b/data/favicon.ico differ
diff --git a/data/index.html b/data/index.html
index f167d3a3847638315026c4c4dce29a8a5f8c1ac9..53830e4ed5bf15a7d443606a8581f9c6ff696fd7 100644
--- a/data/index.html
+++ b/data/index.html
@@ -1,81 +1,42 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-    <title>WebSocketTester</title>
-    <style type="text/css" media="screen">
-    body {
-      margin:0;
-      padding:0;
-      background-color: black;
-    }
+<!doctype html>
+<html lang="en">
 
-    #dbg, #input_div, #input_el {
-      font-family: monaco;
-      font-size: 12px;
-      line-height: 13px;
-      color: #AAA;
-    }
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Matrix of Life</title>
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
+  <link href="styles.css" rel="stylesheet">
+</head>
 
-    #dbg, #input_div {
-      margin:0;
-      padding:0;
-      padding-left:4px;
-    }
+<body class="w-100 h-100 d-flex align-items-center">
+
+  <div class="container justify-content-center">
+
+    <div class="col-lg-6 offset-lg-3">
+
+      <div class="row mb-3">
+        <label for="rangeBrightness" class="col-form-label">Brightness</label>
+        <input type="range" class="form-range" id="rangeBrightness" data-name="brightness" min="8" max="255" steps="1">
+      </div>
+      <div class="row mb-3">
+        <label for="rangeSpeed" class="col-form-label">Speed</label>
+        <input type="range" class="form-range" id="rangeSpeed" data-name="interval" min="10" max="1000" steps="10">
+      </div>
+      <button type="button" class="btn btn-primary" id="btnGlider">Add Glider</button>
+
+      <pre id="dbg"></pre>
+      <div id="input_div">
+        $<input type="text" value="" id="input_el">
+      </div>
 
-    #input_el {
-      width:98%;
-      background-color: rgba(0,0,0,0);
-      border: 0px;
-    }
-    #input_el:focus {
-      outline: none;
-    }
-    </style>
-    <script type="text/javascript">
-    var ws = null;
-    function ge(s){ return document.getElementById(s);}
-    function ce(s){ return document.createElement(s);}
-    function stb(){ window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight); }
-    function addMessage(m){
-      var msg = ce("div");
-      msg.innerText = m;
-      ge("dbg").appendChild(msg);
-      stb();
-    }
-    function startSocket(){
-      ws = new WebSocket('ws://'+document.location.host+'/ws',['arduino']);
-      ws.onopen = function(e){
-        addMessage("Connected");
-      };
-      ws.onclose = function(e){
-        addMessage("Disconnected");
-      };
-      ws.onerror = function(e){
-        console.log("ws error", e);
-        addMessage("Error");
-      };
-      ws.onmessage = function(e){
-        var msg = "> "+e.data;
-        addMessage(msg);
-      };
-      ge("input_el").onkeydown = function(e){
-        stb();
-        if(e.keyCode == 13 && ge("input_el").value != ""){
-          ws.send(ge("input_el").value);
-          ge("input_el").value = "";
-        }
-      }
-    }
-    function onBodyLoad(){
-      startSocket();
-    }
-    </script>
-  </head>
-  <body id="body" onload="onBodyLoad()">
-    <pre id="dbg"></pre>
-    <div id="input_div">
-      $<input type="text" value="" id="input_el">
     </div>
-  </body>
+
+  </div>
+
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
+  <script src="https://code.jquery.com/jquery-3.7.0.slim.min.js" integrity="sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=" crossorigin="anonymous"></script>
+  <script src="main.js"></script>
+</body>
+
 </html>
diff --git a/data/main.js b/data/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..8540a25cdb74ff7027db3ec758ffab2ad0c38e9e
--- /dev/null
+++ b/data/main.js
@@ -0,0 +1,70 @@
+var ws = null;
+function ge(s) { return document.getElementById(s); }
+function ce(s) { return document.createElement(s); }
+function addMessage(m) {
+    var msg = ce("div");
+    msg.innerText = m;
+    ge("dbg").appendChild(msg);
+}
+
+var config = {
+    brightness: 0,
+    interval: 0,
+};
+
+$(document).ready(function() {
+    startSocket();
+});
+
+$(document).on('input change', '#rangeBrightness, #rangeSpeed', function() {
+    config[$(this).data('name')] = $(this).val();
+    var payload = {
+        config: config
+    };
+    ws.send(JSON.stringify(payload));
+});
+
+$('#btnGlider').on('click', function() {
+    var payload = {
+        action: "addGlider"
+    };
+    ws.send(JSON.stringify(payload));
+})
+
+function startSocket() {
+    ws = new WebSocket('ws://' + document.location.host + '/ws', ['arduino']);
+    ws.onopen = function (e) {
+        addMessage("Connected");
+    };
+    ws.onclose = function (e) {
+        addMessage("Disconnected");
+    };
+    ws.onerror = function (e) {
+        console.log("ws error", e);
+        addMessage("Error");
+    };
+    ws.onmessage = function (e) {
+        var msg = "> " + e.data;
+        addMessage(msg);
+        var data = jQuery.parseJSON(e.data);
+        console.log(data);
+        if(data.config) {
+            loadConfig(data.config);
+        } else {
+            console.log("no config");
+        }
+    };
+    ge("input_el").onkeydown = function (e) {
+        if (e.keyCode == 13 && ge("input_el").value != "") {
+            ws.send(ge("input_el").value);
+            ge("input_el").value = "";
+        }
+    }
+}
+
+function loadConfig(data) {
+    config.brightness = data.brightness;
+    config.interval = data.interval;
+    $('#rangeBrightness').val(data.brightness);
+    $('#rangeSpeed').val(data.interval);
+}
diff --git a/data/styles.css b/data/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..3bee0465785fd6c068be7522eaf6cf89483d3d82
--- /dev/null
+++ b/data/styles.css
@@ -0,0 +1,24 @@
+html {
+  width: 100%;
+  height: 100%;
+}
+
+#dbg, #input_div, #input_el {
+  font-family: monaco;
+}
+
+#dbg, #input_div {
+  margin:0;
+  padding:0;
+  padding-left:4px;
+}
+
+#input_el {
+  width:98%;
+  background-color: rgba(0,0,0,0);
+  border: 0px;
+}
+
+#input_el:focus {
+  outline: none;
+}
diff --git a/src/gameoflife.cpp b/src/gameoflife.cpp
index fde43972b1d44103462320eeb9b5f4785a6960c7..d5d57e2c3bd51890df5c9667a327770823b515dd 100644
--- a/src/gameoflife.cpp
+++ b/src/gameoflife.cpp
@@ -34,7 +34,7 @@ void setupGameOfLife()
   createRandomMatrix(g);
   for (int i = 0; i < 10; i++)
   {
-    addGlider(random(SCREEN_HEIGHT), random(SCREEN_WIDTH), g);
+    addGlider();
   }
   logLine("Starting Game " + String(gameEra));
 }
@@ -96,7 +96,7 @@ void gameOfLife(int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH])
   }
 }
 
-void addGlider(int i1, int j1, int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH])
+void createGlider(int i1, int j1, int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH])
 {
   // 010
   // 001
@@ -111,6 +111,10 @@ void addGlider(int i1, int j1, int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH])
   }
 }
 
+void addGlider() {
+  createGlider(random(SCREEN_HEIGHT), random(SCREEN_WIDTH), g);
+}
+
 void gameLoop()
 {
   if (runGame == false)
diff --git a/src/gameoflife.h b/src/gameoflife.h
index 2e0e14c8412616beb080b0df1d464b641bf36f1c..2af799f335e033ec3a929d1ed25c176cfa7d0240 100644
--- a/src/gameoflife.h
+++ b/src/gameoflife.h
@@ -16,7 +16,8 @@ extern int defaultBrightness;
 void setupGameOfLife();
 void createRandomMatrix(int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH]);
 void gameOfLife(int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH]);
-void addGlider(int i1, int j1, int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH]);
+void createGlider(int i1, int j1, int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH]);
+void addGlider();
 void gameLoop();
 void endGame();
 void resetGame();
diff --git a/src/network.cpp b/src/network.cpp
index fc1d8b13576222c4e42a7a924b441d1f073abf14..a13222bfabb358245b2153ee03c5f3ddd1f8ddda 100644
--- a/src/network.cpp
+++ b/src/network.cpp
@@ -4,6 +4,7 @@
 #include "utils.h"
 #include "network.h"
 #include "display.h"
+#include "gameoflife.h"
 
 MDNSResponder mdns;
 AsyncWebServer server(80);
@@ -62,6 +63,7 @@ void setupOTA()
                       clearDisplay(); });
   ArduinoOTA.onEnd([]()
                    {
+                    otaProgress = 0;
                     runGame = true; 
                     logLine("Update End");
                     clearDisplay(); });
@@ -85,6 +87,7 @@ void setupOTA()
     else if(error == OTA_RECEIVE_ERROR) logLine("OTA Receive Failed");
     else if(error == OTA_END_ERROR) logLine("OTA End Failed");
     clearDisplay();
+    otaProgress = 0;
     runGame = true; });
   ArduinoOTA.setHostname(HOSTNAME);
   ArduinoOTA.begin();
@@ -244,6 +247,18 @@ void handleJson(uint8_t *data)
   {
     updateConfig(doc);
   }
+
+  if (doc.containsKey("action"))
+  {
+    if (doc["action"] == "addGlider")
+    {
+      addGlider();
+    }
+    else if (doc["action"] == "reboot")
+    {
+      ESP.restart();
+    }
+  }
 }
 
 void updateConfig(StaticJsonDocument<200U> doc)