Add debug Web app
authorBenjamin Braatz <bb@bbraatz.eu>
Sun, 7 Mar 2021 02:23:49 +0000 (03:23 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Sun, 7 Mar 2021 02:23:49 +0000 (03:23 +0100)
web/controlpi-debug.css [new file with mode: 0644]
web/controlpi-debug.js [new file with mode: 0644]
web/index.css [new file with mode: 0644]
web/index.html [new file with mode: 0644]

diff --git a/web/controlpi-debug.css b/web/controlpi-debug.css
new file mode 100644 (file)
index 0000000..22c072d
--- /dev/null
@@ -0,0 +1,126 @@
+.clientcontainer {
+    padding-right: 5px;
+    padding-bottom: 5px;
+}
+
+.client {
+    display: inline-block;
+    vertical-align: top;
+    width: 300px;
+    margin-top: 5px;
+    margin-left: 5px;
+    border: 2px solid black;
+    padding: 5px;
+    background-color: #bcdaf8;
+}
+
+.client h2 {
+    font-size: 1.2rem;
+    font-weight: bold;
+}
+
+.templatecontainer {
+}
+
+.lastcontainer {
+    text-align: center;
+}
+
+.lastcontainer * {
+    text-align: left;
+}
+
+.templatecontainer h3 {
+    display: inline-block;
+    margin-top: 5px;
+    font-weight: bold;
+}
+
+.message {
+    display: inline-block;
+    vertical-align: top;
+    background-color: #79b5e7;
+    border: 1px solid black;
+}
+
+.message.green {
+    background-color: #85be71;
+}
+
+.message.red {
+    background-color: #ee96a8;
+}
+
+.templatecontainer .message {
+    font-size: 0.8rem;
+    margin-top: 5px;
+    margin-left: 5px;
+}
+
+.lastcontainer .message {
+    margin-top: 5px;
+}
+
+.message span {
+    margin: 5px;
+    font-size: 1rem;
+}
+
+.message h4 {
+    margin: 2px;
+    font-size: 0.8rem;
+}
+
+.message table {
+    border-spacing: 2px;
+}
+
+.message input {
+    background-color: #35bfd3;
+}
+
+.message input[readonly] {
+    background-color: #79b5e7;
+}
+
+.message input:hover,
+.message input:focus {
+    background-color: #afa7ec;
+}
+
+.message input::selection {
+    background-color: #d69ae2;
+}
+
+.message input[type="submit"] {
+    border: 1px solid #0f4a53;
+    border-radius: 12px;
+    padding: 2px;
+}
+
+.message input[type="submit"]:hover,
+.message input[type="submit"]:focus {
+    border: 1px solid #44405e;
+}
+
+.message input[type="text"],
+.message input[type="number"] {
+    min-width: 5px;
+}
+
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+}
+input[type="number"] {
+    -moz-appearance: textfield;
+}
+
+.message select {
+    background-color: #35bfd3;
+}
+
+.message select:hover,
+.message select:focus {
+    background-color: #afa7ec;
+}
diff --git a/web/controlpi-debug.js b/web/controlpi-debug.js
new file mode 100644 (file)
index 0000000..e9e39dd
--- /dev/null
@@ -0,0 +1,302 @@
+// Open Websocket back to ControlPi we were loaded from:
+const websocket = new WebSocket("ws://" + window.location.host)
+
+// When Websocket is ready request all clients from bus:
+websocket.addEventListener('open', function (event) {
+    websocket.send(JSON.stringify({target: '', command: 'get clients'}))
+})
+
+// Create section for client:
+function createForClient(client) {
+    const section = document.createElement('section')
+    section.setAttribute('id', client)
+    section.setAttribute('class', 'client')
+    section.innerHTML = `
+        <h2>${client}</h2>
+        <div id="${client} Receives" class="templatecontainer">
+            <h3>=&gt;</h3>
+        </div>
+        <div id="${client} Sends" class="templatecontainer">
+            <h3>&lt;=</h3>
+        </div>
+        <div id="${client} Last" class="lastcontainer">
+        </div>
+    `
+    return section
+}
+
+// Create div and table for template or message:
+function createForMessage(message, isMessage) {
+    const div = document.createElement('div')
+    div.setAttribute('class', 'message')
+    if (isMessage) {
+        // Current (receive) time as heading:
+        const time = new Date().toLocaleTimeString()
+        const h4 = document.createElement('h4')
+        const h4Content = document.createTextNode(time)
+        h4.appendChild(h4Content)
+        div.appendChild(h4)
+    }
+    if (Object.keys(message).length === 0) {
+        // Create span with '*' for empty templates:
+        const span = document.createElement('span')
+        const spanContent = document.createTextNode('*')
+        span.appendChild(spanContent)
+        div.appendChild(span)
+    } else {
+        const table = document.createElement('table')
+        for (const key in message) {
+            if (isMessage && key == 'sender') {
+                // Ignore 'sender' for messages (redundant in client heading):
+                continue
+            }
+            // Append table row for key-value pair:
+            const tr = document.createElement('tr')
+            const keyTd = document.createElement('td')
+            const keyTdContent = document.createTextNode(key + ':')
+            keyTd.appendChild(keyTdContent)
+            tr.appendChild(keyTd)
+            const valueTd = document.createElement('td')
+            value = JSON.stringify(message[key])
+            if (value.startsWith('"<class') && value.endsWith('>"')) {
+                // Remove quotes if class type instead of literal value:
+                value = value.replace(/^"|"$/g, '')
+            }
+            const valueTdContent = document.createTextNode(value)
+            valueTd.appendChild(valueTdContent)
+            tr.appendChild(valueTd)
+            table.appendChild(tr)
+        }
+        div.appendChild(table)
+    }
+    return div
+}
+
+// Resize input fields to current input:
+function resizeInput() {
+    const tmp = document.createElement('span')
+    // Hack: Should get this from CSS class rather than hardcode it:
+    tmp.setAttribute('style', 'font-size: 0.8rem;')
+    const tmpContent = document.createTextNode(this.value)
+    tmp.appendChild(tmpContent)
+    document.body.appendChild(tmp);
+    const width = tmp.getBoundingClientRect().width
+    document.body.removeChild(tmp);
+    this.style.width = width + 'px'
+}
+
+// Create div, form and table for command template:
+function createForCommand(template) {
+    const div = document.createElement('div')
+    div.setAttribute('class', 'message')
+    const form = document.createElement('form')
+    form.addEventListener("submit", function(e) {
+        // Convert names and values of all form elements to key-value pairs
+        // and send as JSON over websocket:
+        e.preventDefault();
+        var data = {};
+        for (const element of form.elements) {
+            if (element.type == 'text' || element.type == 'hidden') {
+                data[element.name] = element.value
+            } else if (element.type == 'number') {
+                data[element.name] = element.valueAsNumber
+            } else if (element.type == 'select-one') {
+                if (element.value == 'true') {
+                    data[element.name] = true
+                } else if (element.value == 'false') {
+                    data[element.name] = false
+                } else {
+                    data[element.name] = element.value
+                }
+            }
+        }
+        websocket.send(JSON.stringify(data))
+    })
+    const table = document.createElement('table')
+    for (const key in template) {
+        // Append table row for key-value pair:
+        const tr = document.createElement('tr')
+        const keyTd = document.createElement('td')
+        const keyTdContent = document.createTextNode(key + ':')
+        keyTd.appendChild(keyTdContent)
+        tr.appendChild(keyTd)
+        const valueTd = document.createElement('td')
+        value = template[key]
+        // Create submit button for command itself:
+        if (key == 'command') {
+            const hidden = document.createElement('input')
+            hidden.setAttribute('type', 'hidden')
+            hidden.setAttribute('name', key)
+            hidden.setAttribute('value', value)
+            valueTd.appendChild(hidden)
+            const submit = document.createElement('input')
+            submit.setAttribute('type', 'submit')
+            submit.setAttribute('value', value)
+            valueTd.appendChild(submit)
+        // Create appropriate input fields for '<class ...>':
+        } else if (value == '<class \'str\'>') {
+            const openquote = document.createTextNode('"')
+            valueTd.appendChild(openquote)
+            const input = document.createElement('input')
+            input.setAttribute('type', 'text')
+            input.setAttribute('name', key)
+            input.setAttribute('value', '')
+            input.setAttribute('required', '')
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+            valueTd.appendChild(input)
+            const closequote = document.createTextNode('"')
+            valueTd.appendChild(closequote)
+        } else if (value == '<class \'int\'>') {
+            const input = document.createElement('input')
+            input.setAttribute('type', 'number')
+            input.setAttribute('step', '1')
+            input.setAttribute('name', key)
+            input.setAttribute('value', '')
+            input.setAttribute('required', '')
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+            valueTd.appendChild(input)
+        } else if (value == '<class \'float\'>') {
+            const input = document.createElement('input')
+            input.setAttribute('type', 'number')
+            input.setAttribute('step', 'any')
+            input.setAttribute('name', key)
+            input.setAttribute('value', '')
+            input.setAttribute('required', '')
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+            valueTd.appendChild(input)
+        } else if (value == '<class \'bool\'>') {
+            const select = document.createElement('select')
+            select.setAttribute('name', key)
+            const optionTrue = document.createElement('option')
+            optionTrue.setAttribute('value', true)
+            const optionTrueContent = document.createTextNode('true')
+            optionTrue.appendChild(optionTrueContent)
+            select.appendChild(optionTrue)
+            const optionFalse = document.createElement('option')
+            optionFalse.setAttribute('value', false)
+            const optionFalseContent = document.createTextNode('false')
+            optionFalse.appendChild(optionFalseContent)
+            select.appendChild(optionFalse)
+            valueTd.appendChild(select)
+        // Create readonly input fields for literals:
+        } else if (typeof(value) == 'string') {
+            const openquote = document.createTextNode('"')
+            valueTd.appendChild(openquote)
+            const input = document.createElement('input')
+            input.setAttribute('type', 'text')
+            input.setAttribute('name', key)
+            input.setAttribute('value', value)
+            input.setAttribute('readonly', '')
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+            valueTd.appendChild(input)
+            const closequote = document.createTextNode('"')
+            valueTd.appendChild(closequote)
+        } else if (typeof(value) == 'number') {
+            const input = document.createElement('input')
+            input.setAttribute('type', 'number')
+            input.setAttribute('name', key)
+            input.setAttribute('value', value)
+            input.setAttribute('readonly', '')
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+            valueTd.appendChild(input)
+        } else if (typeof(value) == 'boolean') {
+            const select = document.createElement('select')
+            select.setAttribute('name', key)
+            const optionTrue = document.createElement('option')
+            optionTrue.setAttribute('value', 'true')
+            if (value) {
+                optionTrue.setAttribute('selected', '')
+            } else {
+                optionTrue.setAttribute('disabled', '')
+            }
+            const optionTrueContent = document.createTextNode('true')
+            optionTrue.appendChild(optionTrueContent)
+            select.appendChild(optionTrue)
+            const optionFalse = document.createElement('option')
+            optionFalse.setAttribute('value', 'false')
+            if (value) {
+                optionTrue.setAttribute('disabled', '')
+            } else {
+                optionTrue.setAttribute('selected', '')
+            }
+            const optionFalseContent = document.createTextNode('false')
+            optionFalse.appendChild(optionFalseContent)
+            select.appendChild(optionFalse)
+            valueTd.appendChild(select)
+        }
+        tr.appendChild(valueTd)
+        table.appendChild(tr)
+    }
+    form.appendChild(table)
+    div.appendChild(form)
+    return div
+}
+
+// When receiving message from ControlPi through Websocket:
+websocket.addEventListener('message', function (event) {
+    const message = JSON.parse(event.data)
+    if (message.sender == '') {
+        // When message is from bus:
+        if (message.event == 'unregistered') {
+            // On deregistration delete section if it exists:
+            const clientElement = document.getElementById(message.client)
+            if (clientElement != null) {
+                clientElement.remove()
+            }
+        } else {
+            // On registration or initial answers for all clients:
+            const clientElement = document.getElementById(message.client)
+            if (clientElement == null) {
+                // Create section for client if not existent:
+                const main = document.getElementById('ControlPi Debug')
+                main.appendChild(createForClient(message.client))
+            }
+            const receiveContainer = document.getElementById(message.client + ' Receives')
+            receiveContainer.innerHTML = '<h3>=&gt;</h3>'
+            for (const template of message.receives) {
+                if (template.command != null) {
+                    const templateElement = createForCommand(template)
+                    receiveContainer.appendChild(templateElement)
+                } else {
+                    const templateElement = createForMessage(template)
+                    receiveContainer.appendChild(templateElement)
+                }
+            }
+            const sendContainer = document.getElementById(message.client + ' Sends')
+            sendContainer.innerHTML = '<h3>&lt;=</h3>'
+            for (const template of message.sends) {
+                const templateElement = createForMessage(template)
+                sendContainer.appendChild(templateElement)
+            }
+        }
+    } else {
+        // When message is from client:
+        const clientElement = document.getElementById(message.sender)
+        if (clientElement == null) {
+            // Create section for client if not existent:
+            const main = document.getElementById('ControlPi Debug')
+            main.appendChild(createForClient(message.sender))
+        }
+        // Update last received message:
+        const lastContainer = document.getElementById(message.sender + ' Last')
+        lastContainer.innerHTML = ''
+        const messageElement = createForMessage(message, true)
+        lastContainer.appendChild(messageElement)
+        if (message.state != null) {
+            // If this is some kind of state set background
+            // green or red depending on it:
+            if (message.state) {
+                messageElement.classList.remove('red')
+                messageElement.classList.add('green')
+            } else {
+                messageElement.classList.remove('green')
+                messageElement.classList.add('red')
+            }
+        }
+    }
+})
diff --git a/web/index.css b/web/index.css
new file mode 100644 (file)
index 0000000..e33bc74
--- /dev/null
@@ -0,0 +1,61 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto');
+@import url('https://fonts.googleapis.com/css?family=Michroma');
+
+* {
+    vertical-align: baseline;
+       margin: 0;
+       outline: 0;
+       border: 0 none;
+       padding: 0;
+    font-family: 'Roboto', sans-serif;
+       font-weight: inherit;
+       font-style: inherit;
+       font-size: inherit;
+}
+
+html {
+    font-size: 16px;
+}
+
+body {
+    background-color: white;
+    color: black;
+}
+
+header {
+    padding: 10px;
+    background-color: #2c343b;
+}
+
+header * {
+    vertical-align: middle;
+}
+
+header svg {
+    height: 40px;
+}
+
+header span {
+    font-family: "Michroma", sans-serif;
+    font-weight: bold;
+    font-size: 20px;
+    text-transform: uppercase;
+}
+
+header span.Graph {
+    margin-left: 10px;
+    color: white;
+}
+
+header span.IT {
+    margin-right: 10px;
+    color: #83a1b4;
+}
+
+h1 {
+    margin-top: 5px;
+    margin-left: 5px;
+    margin-right: 5px;
+    font-size: 1.4rem;
+    font-weight: bold;
+}
diff --git a/web/index.html b/web/index.html
new file mode 100644 (file)
index 0000000..f6d6ff5
--- /dev/null
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <title>ControlPi Debug</title>
+        <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
+        <link rel="stylesheet" type="text/css" href="index.css">
+        <link rel="stylesheet" type="text/css" href="controlpi-debug.css">
+        <script src="controlpi-debug.js" defer></script>
+    </head>
+    <body>
+        <header>
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 334 299">
+                <g stroke="#83a1b4" stroke-width="8" fill="none">
+                    <circle r="14.5" transform="translate(18.53 235.35)"/>
+                    <circle r="14.5" transform="translate(40.39 75.08)"/>
+                    <circle r="14.5" transform="translate(198.04 20.48)"/>
+                    <circle r="14.5" transform="translate(313.77 145.08)"/>
+                    <circle r="14.5" transform="translate(145.7 131.46)"/>
+                    <circle r="14.5" transform="translate(209.99 279.89)"/>
+                    <path stroke-width="3.7" d="M38.8 241l149 34.8m34.7-12.3l77-100m-261 67l253.4-78M34 222.5L128 146M20.4 214.7l16-117m23.3-29l116.8-41.8M154 112.6L188 39m-21.5 95L291 144M58.6 86.5l67 35.7m86.2-86.6L298 130"/>
+                </g>
+            </svg>
+            <span class="Graph">Graph-</span><span class="IT">IT</span>
+        </header>
+        <h1>ControlPi Debug</h1>
+        <main id="ControlPi Debug" class="clientcontainer">
+        </main>
+    </body>
+</html>