diff --git a/tools/proxy_node.html b/tools/proxy_node.html
index 13c94d6691673d089cf32006f89688941b99b7d8..e1541f3cfd6476482242381e04da7661dc35136a 100644
--- a/tools/proxy_node.html
+++ b/tools/proxy_node.html
@@ -1,190 +1,78 @@
 <!DOCTYPE html>
 <html lang="en">
-	<head>
-		<meta charset="utf-8" />
-		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-		<title>OpenDHT tester</title>
-		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
-        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" />
-		<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
-		<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
-		<script type="text/javascript">"use strict";
-let onGet;
-let onPut;
-let setServer;
-const valueGetElement = function(o) {
-    const d = window.atob(o.data);
-    return d;
-};
-$(function() {
-    let request = undefined;
-    let server;
-    const getTools = $("#getTools");
-    const getBtn = $("#getBtn");
-    const getDropdown = $("#getDropdown");
-    const listenBtn = $("#listenBtn").click(function(){onGet('LISTEN');});
-    const setGetRequest = function() {
-        getBtn.button('loading');
-        getStopBtn.appendTo(getTools);
-        getDropdown.hide();
-    }
-    const clearGetRequest = function() {
-        if (request === undefined)
-            return;
-        request.abort();
-        request = undefined;
-        getStopBtn.detach();
-        getDropdown.show();
-        getBtn.button('reset');
-    }
-    const getStopBtn = $("#getStopBtn").detach().click(clearGetRequest);
-    const putBtn = $("#putBtn");
-    const result = $("#dhtResult");
-    const group = $('<ul class="list-group"/>').appendTo(result);
-    onGet = function (method) {
-        if (request !== undefined)
-            return false;
-        const input = $("#getKey").val();
-        group.empty();
-        let lastAppended = 0;
-        let start = new Date().getTime();
-        request = new XMLHttpRequest();
-        request.onreadystatechange = function(event) {
-            if (this.readyState >= XMLHttpRequest.LOADING) {
-                if (this.readyState == XMLHttpRequest.DONE) {
-                    clearGetRequest();
-                }
-                if (this.status === 200) {
-                    const elements = this.responseText.split("\n");
-                    const elementsLength = elements.length;
-                    const now = new Date().getTime();
-                    for (let i = lastAppended; i < elementsLength; i++) {
-                        const element = elements[i];
-                        if (!element || element.length == 0)
-                            return;
-                        const o = JSON.parse(element);
-                        if (o.expired) {
-                            $('#value'+o.id).slideUp(100, (e) => $(e.target).remove());
-                        } else {
-                            const d = window.atob(o.data);
-                            const delay = Math.max(0, start-now);
-                            $('<li class="list-group-item" id="value'+o.id+'"/>').append(valueGetElement(o)).appendTo(group).hide().delay(delay).slideDown(100);
-                            lastAppended = i+1;
-                            start = Math.max(start, now)+25;
-                        }
-                    }
-                } else if (this.status !== 0) {
-                    group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content: " + this.statusText));
-                }
-            }
-        };
-        request.onerror = function(event) {
-            clearGetRequest();
-            group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content."));
-        };
-        request.open(method, server + input, true);
-        request.send(null);
-        setGetRequest();
-        return false;
-    };
-
-    onPut = function( ) {
-        const key = $("#getKey").val();
-        const value = $("#putValue").val();
-        $.ajax({
-            url: server + key,
-            type: 'POST',
-            data: JSON.stringify({
-                data:window.btoa(value)
-            }),
-            contentType: 'application/json; charset=utf-8',
-            dataType: 'json',
-            success: function( result ) {
-                putBtn.button('reset');
-                //$('<li class="list-group-item list-group-item-success"/>').append(valueGetElement(result)).appendTo(group.empty());
-            },
-            error: function(result) {
-                putBtn.button('reset');
-                group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text(result.statusText));
-            }
-        });
-        putBtn.button('loading');
-        return false;
-    };
-
-    const serverValue = $("#serverValue");
-    const serverStatus = $("#serverStatus");
-    const serverBtn = $("#serverBtn");
-    setServer = function(event) {
-        server = serverValue.val() + '/';
-        serverStatus.empty();
-        serverBtn.button('loading');
-        $.getJSON(server, function(data){
-            $('<span><b>Node</b> '+data.node_id+'</span>').appendTo(serverStatus).hide().fadeIn();
-        }).fail(function(error) {
-            var message = " Cant' access node."
-            if (serverValue.val().indexOf("https") != -1){
-                message += "</br></br>Self-signed certificate must be allowed in browser."
-            }
-            serverStatus.html("<div class='alert alert-danger' style='margin-bottom: 0px;'>" +
-                              "<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>" +
-                              message + "</div>");
-        }).always(function(error) {
-            serverBtn.button('reset');
-        });
-        return false;
-    };
-    setServer();
-});
-	</script>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>OpenDHT web node</title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.3/css/bootstrap.min.css" />
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
 </head>
+
 <body>
-    <div class="container" style="max-width: 730px;">
-        <header class="page-header">
+    <div class="container" style="max-width: 880px;">
+        <header class="m-3 p-3">
             <div class="row">
-                <div class="col-sm-5">
-                    <h1>OpenDHT tester</h1>
+                <div class="col-sm-5 d-flex align-items-center">
+                    <h2>OpenDHT web node</h2>
                 </div>
                 <div class="col-sm-7">
-                    <div class="well well-sm" style="margin-top:10px; margin-bottom:0px;">
-                        <form id="serverForm" class="form-inline" onsubmit="return setServer();" style="margin-bottom:4px;">
+                    <div class="card card-body bg-light" style="margin-top:10px; margin-bottom:0px;">
+                        <form id="serverForm" class="form-inline" onsubmit="return setServer();"
+                            style="margin-bottom:4px;">
                             <div class="input-group">
-                                <input type="text" class="form-control" id="serverValue" placeholder="Proxy server" value="http://127.0.0.1:8080"/>
-                                <span class="input-group-btn">
-                                    <button id="serverBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span></button>
-                                </span>
+                                <input type="text" class="form-control" id="serverValue" placeholder="Proxy server" value="http://127.0.0.1:8080" />
+                                <button id="serverBtn" type="submit" class="btn btn-outline-secondary">
+                                    <div class='spinner-border spinner-border-sm' role='status'
+                                        style="display: none;">
+                                        <span class='visually-hidden'>Loading...</span>
+                                    </div>
+                                    <span class="btn-icon"><i class="bi bi-arrow-clockwise"></i></span>
+                                </button>
                             </div>
                         </form>
-                        <div id="serverStatus"><i class='fa fa-circle-o-notch fa-spin'></i></div>
+                        <div id="serverStatus">
+                            <div class='spinner-border spinner-border-sm' role='status'><span
+                                    class='visually-hidden'>Loading...</span></div>
+                        </div>
                     </div>
                 </div>
             </div>
         </header>
-        <div class="panel panel-default" id="dhtResult">
-                <div class="panel-heading">
-                    <div class="row">
-                    <div class="col-xs-6">
-                        <form class="form-inline" onsubmit="return onGet('GET');">
-                            <div class="input-group">
+        <div class="list-group" id="dhtResult">
+            <div class="list-group-item list-group-item-light">
+                <div class="row">
+                    <div class="col-sm-6">
+                        <form class="form-inline" onsubmit="return onGet('');">
+                            <div class="input-group" id="getTools">
                                 <input type="text" class="form-control" id="getKey" placeholder="Key" aria-label="Key" />
-                                <span class="input-group-btn" id="getTools">
-                                    <button id="getBtn" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>" type="submit">Get</button>
-                                    <button id="getDropdown"type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button>
-                                    <ul class="dropdown-menu">
-                                        <li><a id="listenBtn" href="#">Listen</a></li>
-                                    </ul>
-                                    <button id="getStopBtn" class="btn btn-default" type="submit"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
-                                </span>
+                                <button id="getBtn" class="btn btn-outline-secondary" type="submit">
+                                    <div class='spinner-border spinner-border-sm' role='status' style="display: none;">
+                                        <span class='visually-hidden'>Loading...</span>
+                                    </div>
+                                    <span class="btn-text">Get</span>
+                                </button>
+                                <button id="menuBtn" type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
+                                    <span class="visually-hidden">Toggle Dropdown</span>
+                                </button>
+                                <ul class="dropdown-menu dropdown-menu-end">
+                                    <li><a class="dropdown-item" id="listenBtn" href="#">Listen</a></li>
+                                </ul>
+                                <button id="getStopBtn" class="btn btn-outline-secondary">Stop</button>
                             </div>
                         </form>
                     </div>
-                    <div class="col-xs-6">
+                    <div class="col-sm-6">
                         <form class="form-inline" onsubmit="return onPut();">
                             <div class="input-group">
-                                <input type="text" class="form-control input-group-input" id="putValue" placeholder="Value" />
-                                <span class="input-group-btn">
-                                    <button id="putBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Loading">Put</button>
-                                </span>
+                                <input type="text" class="form-control input-group-input" id="putValue"
+                                    placeholder="Value" />
+                                <button id="putBtn" type="submit" class="btn btn-outline-secondary">
+                                    <div class='spinner-border spinner-border-sm' role='status' style="display: none;">
+                                        <span class='visually-hidden'>Loading...</span>
+                                    </div>
+                                    <span class="btn-text">Put</span>
+                                </button>
                             </div>
                         </form>
                     </div>
@@ -192,5 +80,185 @@ $(function() {
             </div>
         </div>
     </div>
+
+    <script type="text/javascript">"use strict";
+        const valueGetElement = (o) => window.atob(o.data);
+        const getTools = document.getElementById("getTools");
+        const getBtn = document.getElementById("getBtn");
+        const menuBtn = document.getElementById("menuBtn");
+        const getStopBtn = document.getElementById("getStopBtn");
+        const listenBtn = document.getElementById("listenBtn");
+        const putBtn = document.getElementById("putBtn");
+        const group = document.getElementById("dhtResult");
+        const serverValue = document.getElementById("serverValue");
+        const serverStatus = document.getElementById("serverStatus");
+        const serverBtn = document.getElementById("serverBtn");
+
+        var request = undefined;
+        const setGetRequest = () => {
+            getBtn.disabled = true;
+            getBtn.querySelector('.spinner-border').style.display = 'inline-block';
+            getBtn.querySelector('.btn-text').style.display = 'none';
+            menuBtn.style.display = 'none';
+            getTools.appendChild(getStopBtn);
+        };
+        const clearGetRequest = () => {
+            if (request === undefined)
+                return;
+            request.abort();
+            request = undefined;
+            if (getStopBtn.parentNode) {
+                getStopBtn.parentNode.removeChild(getStopBtn);
+            }
+            menuBtn.style.display = '';
+            getBtn.disabled = false;
+            getBtn.querySelector('.spinner-border').style.display = 'none';
+            getBtn.querySelector('.btn-text').style.display = 'inline';
+        };
+
+        const clearResults = () => {
+            while (group.children.length > 1) {
+                group.removeChild(group.lastChild);
+            }
+        };
+
+        var server;
+        const setServer = (event) => {
+            server = serverValue.value + '/';
+            serverStatus.innerHTML = '';
+            serverBtn.disabled = true;
+            serverBtn.querySelector('.spinner-border').style.display = 'inline-block';
+            serverBtn.querySelector('.btn-icon').style.display = 'none';
+            var xhr = new XMLHttpRequest();
+            xhr.open('GET', server, true);
+            xhr.onreadystatechange = () => {
+                if (xhr.readyState === XMLHttpRequest.DONE) {
+                    serverBtn.disabled = false;
+                    serverBtn.querySelector('.spinner-border').style.display = 'none';
+                    serverBtn.querySelector('.btn-icon').style.display = 'inline';
+                    if (xhr.status === 200) {
+                        const data = JSON.parse(xhr.responseText);
+                        const span = document.createElement('span');
+                        span.innerHTML = '<b>Node</b> ' + data.node_id;
+                        serverStatus.appendChild(span);
+                    } else {
+                        const message = " Can't access node.";
+                        if (serverValue.value.indexOf("https") != -1) {
+                            message += "</br></br>Self-signed certificate must be allowed in browser.";
+                        }
+                        serverStatus.innerHTML = "<div class='alert alert-danger' style='margin-bottom: 0px;'>" +
+                            "<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>" +
+                            message + "</div>";
+                    }
+                }
+            };
+            xhr.send();
+            return false;
+        };
+        setServer();
+
+        const onGet = (path) => {
+            if (request !== undefined)
+                return false;
+            const input = document.getElementById("getKey").value;
+            clearResults()
+            let lastAppended = 0;
+            let start = new Date().getTime();
+            request = new XMLHttpRequest();
+            request.onreadystatechange = (event) => {
+                if (request.readyState >= 2) { // LOADING
+                    if (request.status === 200) {
+                        const elements = request.responseText.split("\n");
+                        const elementsLength = elements.length;
+                        const now = new Date().getTime();
+                        for (let i = lastAppended; i < elementsLength; i++) {
+                            const element = elements[i];
+                            if (!element || element.length == 0)
+                                break;
+                            const o = JSON.parse(element);
+                            if (o.expired) {
+                                const expiredElement = document.getElementById('value' + o.id);
+                                if (expiredElement) {
+                                    expiredElement.style.display = 'none';
+                                    if (expiredElement.parentNode) {
+                                        expiredElement.parentNode.removeChild(expiredElement);
+                                    }
+                                }
+                            } else {
+                                const d = window.atob(o.data);
+                                const delay = Math.max(0, start - now);
+                                const li = document.createElement('li');
+                                li.className = 'list-group-item';
+                                li.id = 'value' + o.id;
+                                li.innerHTML = valueGetElement(o);
+                                li.style.opacity = 0;
+                                li.style.transition = 'opacity 0.1s ease-in';
+                                group.appendChild(li);
+                                setTimeout(() => {
+                                    li.style.opacity = 1;
+                                }, delay);
+                                lastAppended = i + 1;
+                                start = Math.max(start, now) + 25;
+                            }
+                        }
+                    } else if (request.status !== 0) {
+                        clearResults()
+                        const li = document.createElement('li');
+                        li.className = 'list-group-item list-group-item-danger';
+                        li.textContent = "Error loading content: " + request.statusText;
+                        group.appendChild(li);
+                    }
+                }
+                if (request.readyState == 4) { // DONE
+                    clearGetRequest();
+                }
+            };
+            request.onerror = (event) => {
+                clearGetRequest();
+                clearResults()
+                const li = document.createElement('li');
+                li.className = 'list-group-item list-group-item-danger';
+                li.textContent = "Error loading content.";
+                group.appendChild(li);
+            };
+            request.open('GET', server + 'key/' + input + path, true);
+            request.send(null);
+            setGetRequest();
+            return false;
+        };
+        const onPut = () => {
+            const key = document.getElementById("getKey").value;
+            const value = document.getElementById("putValue").value;
+            putBtn.disabled = true;
+            putBtn.querySelector('.spinner-border').style.display = 'inline-block';
+            putBtn.querySelector('.btn-text').style.display = 'none';
+            fetch(server + key, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json; charset=utf-8' },
+                body: JSON.stringify({ data: window.btoa(value) })
+            })
+            .then(response => {
+                if (!response.ok)
+                    throw new Error(response.statusText);
+            })
+            .catch(error => {
+                group.innerHTML = '';
+                const li = document.createElement('li');
+                li.className = 'list-group-item list-group-item-danger';
+                li.textContent = error.message;
+                group.appendChild(li);
+            })
+            .finally(() => {
+                putBtn.disabled = false;
+                putBtn.querySelector('.spinner-border').style.display = 'none';
+                putBtn.querySelector('.btn-text').style.display = 'inline';
+            });
+            return false;
+        };
+
+        listenBtn.addEventListener('click', () => { onGet('/listen'); });
+        getStopBtn.parentNode.removeChild(getStopBtn);
+        getStopBtn.addEventListener('click', clearGetRequest);
+    </script>
 </body>
-</html>
+</html>
\ No newline at end of file