diff --git a/tools/web_monitor/__init__.py b/tools/web_monitor/__init__.py
new file mode 100644
index 00000000..2687e47d
--- /dev/null
+++ b/tools/web_monitor/__init__.py
@@ -0,0 +1,5 @@
+from tools.web_monitor.app import app
+
+if __name__ == '__main__':
+ # app.run for running the sanic app inside the file
+ app.run(host="0.0.0.0", port=80)
diff --git a/tools/web_monitor/app.py b/tools/web_monitor/app.py
new file mode 100644
index 00000000..650704b0
--- /dev/null
+++ b/tools/web_monitor/app.py
@@ -0,0 +1,13 @@
+import socketio
+from sanic import Sanic
+
+app = Sanic("App")
+
+# attach socketio
+sio = socketio.AsyncServer(async_mode="sanic")
+sio.attach(app)
+
+app.static('/', "./public/index.html")
+app.static('/index.css', "./static/index.css")
+
+app.static('/scan', "./public/scan.html")
diff --git a/tools/web_monitor/index.py b/tools/web_monitor/index.py
new file mode 100644
index 00000000..d9e0388a
--- /dev/null
+++ b/tools/web_monitor/index.py
@@ -0,0 +1,7 @@
+from tools.web_monitor.app import sio
+
+
+@sio.event
+async def connect(sid, _environ) -> None:
+ """Event for connection"""
+ await sio.emit('init', "hello")
diff --git a/tools/web_monitor/public/index.html b/tools/web_monitor/public/index.html
new file mode 100644
index 00000000..e001c418
--- /dev/null
+++ b/tools/web_monitor/public/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
diff --git a/tools/web_monitor/scan_page.py b/tools/web_monitor/scan_page.py
new file mode 100644
index 00000000..af5b2ea5
--- /dev/null
+++ b/tools/web_monitor/scan_page.py
@@ -0,0 +1,16 @@
+from tools.web_monitor.app import sio
+import json
+
+
+async def scan_found_miner(miner):
+ """Send data to client that a miner was scanned.
+
+ :param miner: The miner object that was scanned.
+ """
+ await sio.emit('scan_found_miner', json.dumps(
+ {
+ "ip": str(miner.ip),
+ "model": str(miner.model),
+ "api": str(miner.api_type)
+ }
+ ))
diff --git a/tools/web_monitor/static/index.css b/tools/web_monitor/static/index.css
new file mode 100644
index 00000000..6949a379
--- /dev/null
+++ b/tools/web_monitor/static/index.css
@@ -0,0 +1,89 @@
+body {
+ min-height: 100vh;
+ min-height: -webkit-fill-available;
+}
+
+html {
+ height: -webkit-fill-available;
+}
+
+main {
+ display: flex;
+ flex-wrap: nowrap;
+ height: 100vh;
+ height: -webkit-fill-available;
+ max-height: 100vh;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.b-example-divider {
+ flex-shrink: 0;
+ width: 1.5rem;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, .1);
+ border: solid rgba(0, 0, 0, .15);
+ border-width: 1px 0;
+ box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
+}
+
+.bi {
+ vertical-align: -.125em;
+ pointer-events: none;
+ fill: currentColor;
+}
+
+.dropdown-toggle { outline: 0; }
+
+.nav-flush .nav-link {
+ border-radius: 0;
+}
+
+.btn-toggle {
+ display: inline-flex;
+ align-items: center;
+ padding: .25rem .5rem;
+ font-weight: 600;
+ color: rgba(0, 0, 0, .65);
+ background-color: transparent;
+ border: 0;
+}
+.btn-toggle:hover,
+.btn-toggle:focus {
+ color: rgba(0, 0, 0, .85);
+ background-color: #d2f4ea;
+}
+
+.btn-toggle::before {
+ width: 1.25em;
+ line-height: 0;
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+ transition: transform .35s ease;
+ transform-origin: .5em 50%;
+}
+
+.btn-toggle[aria-expanded="true"] {
+ color: rgba(0, 0, 0, .85);
+}
+.btn-toggle[aria-expanded="true"]::before {
+ transform: rotate(90deg);
+}
+
+.btn-toggle-nav a {
+ display: inline-flex;
+ padding: .1875rem .5rem;
+ margin-top: .125rem;
+ margin-left: 1.25rem;
+ text-decoration: none;
+}
+.btn-toggle-nav a:hover,
+.btn-toggle-nav a:focus {
+ background-color: #d2f4ea;
+}
+
+.scrollarea {
+ overflow-y: auto;
+}
+
+.fw-semibold { font-weight: 600; }
+.lh-tight { line-height: 1.25; }