diff --git a/ssh/list.go b/ssh/list.go index 70ad493..56ba0da 100644 --- a/ssh/list.go +++ b/ssh/list.go @@ -7,15 +7,15 @@ import ( ) type List struct { - cmd string - Clients map[string]*ListResult + cmd string `json:"cmd"` + Clients map[string]*ListResult `json:"clients"` sshManager *Manager } type ListResult struct { ssh *ssh.Client - Runned bool - WithError bool - Result string + Running bool `json:"running"` + WithError bool `json:"with_error"` + Result string `json:"result"` } func (m *Manager) CreateList(cmd string) *List { @@ -25,7 +25,7 @@ func (m *Manager) CreateList(cmd string) *List { Clients: make(map[string]*ListResult), } for host, client := range m.clients { - list.Clients[host] = &ListResult{Runned: false, ssh: client} + list.Clients[host] = &ListResult{Running: true, ssh: client} } return list } @@ -43,7 +43,7 @@ func (l List) Run() { func (l List) runlistelement(host string, client *ListResult, wg *sync.WaitGroup) { defer wg.Done() result, err := l.sshManager.run(host, client.ssh, l.cmd) - client.Runned = true + client.Running = false if err != nil { client.WithError = true return diff --git a/ssh/list_test.go b/ssh/list_test.go index 7461d1f..30bc73f 100644 --- a/ssh/list_test.go +++ b/ssh/list_test.go @@ -19,18 +19,18 @@ func TestList(t *testing.T) { list := mgmt.CreateList("exit 1") assert.Len(list.Clients, 1) client := list.Clients[addr.IP.String()] - assert.False(client.Runned) + assert.True(client.Running) list.Run() - assert.True(client.Runned) + assert.False(client.Running) assert.True(client.WithError) assert.Equal("", client.Result) list = mgmt.CreateList("echo 15") assert.Len(list.Clients, 1) client = list.Clients[addr.IP.String()] - assert.False(client.Runned) + assert.True(client.Running) list.Run() - assert.True(client.Runned) + assert.False(client.Running) assert.False(client.WithError) assert.Equal("15", client.Result) } diff --git a/webroot/css/console.css b/webroot/css/console.css new file mode 100644 index 0000000..4d2843b --- /dev/null +++ b/webroot/css/console.css @@ -0,0 +1,36 @@ +.prompt { + position: fixed; + bottom: 0px; + background-color: #ccc; + width: 100%; +} +.prompt .btn { + width: 10%; +} +.prompt input { + width: 85%; + margin-left: 2%; +} +.console { + font-family: monospace; + white-space: pre; +} +.console .cmd { + min-height: 22px; + clear: both; +} +.console .cmd > .time, .console .cmd .host{ + display: inline-block; + color: #009ee0; + + width: 15%; +} +.console .cmd > div { + clear: both; + background-color: #ccc; + margin-bottom: 3px; +} +.console .cmd .status { + width: 15%; + height: 20px; +} diff --git a/webroot/css/main.css b/webroot/css/main.css index b70d477..fd6a5b7 100644 --- a/webroot/css/main.css +++ b/webroot/css/main.css @@ -9,14 +9,16 @@ body { .status { float: right; background: #009ee0; + color: white; width: 50px; height: 50px; } -.status.connecting { +.status.connecting,.status.running { background: #ffb400; } -.status.offline { +.status.offline, .status.failed { background: #dc0067; + color: white; } span.online { color: #009ee0; diff --git a/webroot/index.html b/webroot/index.html index 89fb38b..5c0a1b8 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -7,6 +7,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/webroot/js/gui.js b/webroot/js/gui.js index af1e74b..efe1b94 100644 --- a/webroot/js/gui.js +++ b/webroot/js/gui.js @@ -1,5 +1,5 @@ /* exported gui,router */ -/* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode */ +/* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode,guiConsole */ const gui = {}, router = new Navigo(null, true, '#'); @@ -58,6 +58,9 @@ const gui = {}, } router.on({ + '/console': function routerConsole () { + setView(guiConsole); + }, '/list': function routerList () { setView(guiList); }, @@ -75,6 +78,7 @@ const gui = {}, '/statistics': function routerStats () { setView(guiStats); } + }); router.on(() => { router.navigate('/list'); diff --git a/webroot/js/gui_console.js b/webroot/js/gui_console.js new file mode 100644 index 0000000..d407e46 --- /dev/null +++ b/webroot/js/gui_console.js @@ -0,0 +1,256 @@ +/* exported guiConsole */ +/* globals domlib,store,socket */ +const guiConsole = {}; + +(function init () { + 'use strict'; + + const view = guiConsole, + ownCMDs = ['0'], + cmdRow = {}; + let container = null, + el = null, + output = null, + ownfilter = false; + + function createID () { + let digit = new Date().getTime(); + + // Use high-precision timer if available + /* eslint-disable */ + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { + digit += performance.now(); + } + + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => { + const result = (digit + Math.random() * 16) % 16 | 0; + + digit = Math.floor(digit / 16); + + return (char === 'x' + ? result + : result & 0x3 | 0x8).toString(16); + }); + /* eslint-enable*/ + } + + function updateCMD (row, cmd) { + if (cmd.cmd === '' && cmd.timestemp === 0) { + return; + } + row.cmd.innerHTML = cmd.cmd; + row.timestemp.innerHTML = moment(cmd.timestemp).fromNow(true); + + let running = 0, + failed = 0, + sum = 0; + + if (cmd.clients) { + sum = Object.keys(cmd.clients).length; + + Object.keys(cmd.clients).forEach((addr) => { + const client = cmd.clients[addr], + clientRow = row.clients[addr]; + + clientRow.status.classList.remove('running', 'failed', 'success'); + if (client.running) { + running += 1; + clientRow.status.classList.add('running'); + } else if (client.with_error) { + failed += 1; + clientRow.status.classList.add('failed'); + } else { + clientRow.status.classList.add('success'); + } + + clientRow.result.innerHTML = client.result; + clientRow.host.innerHTML = addr; + }); + } + + row.status.classList.remove('running', 'failed', 'success'); + if (running > 0) { + row.status.innerHTML = `running (${running}`; + row.status.classList.add('running'); + } else if (failed > 0 || sum === 0) { + row.status.innerHTML = `failed (${failed}`; + row.status.classList.add('failed'); + } else { + row.status.innerHTML = `success (${sum}`; + row.status.classList.add('success'); + } + row.status.innerHTML += `/${sum})`; + } + + function createRow (cmd) { + const row = { + 'clients': {}, + 'clientsContainer': document.createElement('div'), + 'clientsEl': {}, + 'el': document.createElement('div') + }; + + + if (cmd.cmd === '' && cmd.timestemp === 0) { + row.el.innerHTML = '\n' + + ' _______ ________ __\n' + + ' | |.-----.-----.-----.| | | |.----.| |_\n' + + ' | - || _ | -__| || | | || _|| _|\n' + + ' |_______|| __|_____|__|__||________||__| |____|\n' + + ' |__| W I R E L E S S F R E E D O M\n' + + ' -----------------------------------------------------\n' + + ' FreifunkManager shell for openwrt/Lede/gluon systems \n' + + ' -----------------------------------------------------\n' + + ' * 1 1/2 oz Gin Shake with a glassful\n' + + ' * 1/4 oz Triple Sec of broken ice and pour\n' + + ' * 3/4 oz Lime Juice unstrained into a goblet.\n' + + ' * 1 1/2 oz Orange Juice\n' + + ' * 1 tsp. Grenadine Syrup\n' + + ' -----------------------------------------------------\n'; + + return row; + } + row.timestemp = domlib.newAt(row.el, 'span'); + row.cmd = domlib.newAt(row.el, 'span'); + row.status = domlib.newAt(row.el, 'span'); + + row.el.classList.add('cmd'); + row.timestemp.classList.add('time'); + row.status.classList.add('status'); + + if (cmd.clients) { + Object.keys(cmd.clients).forEach((addr) => { + const clientEl = domlib.newAt(row.clientsContainer, 'div'), + clients = { + 'host': domlib.newAt(clientEl, 'span'), + 'result': domlib.newAt(clientEl, 'span'), + 'status': domlib.newAt(clientEl, 'span') + }; + + clients.host.classList.add('host'); + clients.status.classList.add('status'); + + row.clientsEl[addr] = clientEl; + row.clients[addr] = clients; + }); + row.cmd.addEventListener('click', () => { + if (row.clientsContainer.parentElement) { + row.el.removeChild(row.clientsContainer); + } else { + row.el.appendChild(row.clientsContainer); + } + }); + } + + + updateCMD(row, cmd); + + return row; + } + + function update () { + let cmds = store.getCMDs(); + + if (ownfilter) { + const tmp = cmds; + + cmds = {}; + Object.keys(tmp). + forEach((id) => { + if (ownCMDs.indexOf(id) >= 0) { + cmds[id] = tmp[id]; + } + }); + } + Object.keys(cmdRow).forEach((id) => { + if (cmdRow[id].el.parentElement) { + output.removeChild(cmdRow[id].el); + } + }); + + Object.keys(cmds).forEach((id) => { + const cmd = cmds[id]; + + if (cmdRow[id]) { + updateCMD(cmdRow[id], cmd); + } else { + cmdRow[id] = createRow(cmd); + } + }); + + Object.keys(cmdRow). + sort((aID, bID) => { + if (!cmds[aID] || !cmds[bID]) { + return 0; + } + + return cmds[aID].timestemp - cmds[bID].timestemp; + }). + forEach((id) => { + if (cmds[id] && !cmdRow[id].el.parentElement) { + output.appendChild(cmdRow[id].el); + } + }); + } + + view.bind = function bind (bindEl) { + container = bindEl; + }; + view.render = function render () { + if (!container) { + return; + } else if (el) { + container.appendChild(el); + update(); + + return; + } + console.log('generate new view for console'); + el = domlib.newAt(container, 'div'); + + store.updateCMD({ + 'cmd': '', + 'id': '0', + 'timestemp': 0 + }); + + output = domlib.newAt(el, 'div'); + output.classList.add('console'); + + const prompt = domlib.newAt(el, 'div'), + filterBtn = domlib.newAt(prompt, 'span'), + promptInput = domlib.newAt(prompt, 'input'); + + prompt.classList.add('prompt'); + + promptInput.addEventListener('keyup', (event) => { + // eslint-disable-next-line no-magic-numbers + if (event.keyCode !== 13) { + return; + } + const cmd = { + 'cmd': promptInput.value, + 'id': createID(), + 'timestemp': new Date() + }; + + ownCMDs.push(cmd.id); + socket.sendcmd(cmd); + promptInput.value = ''; + }); + + filterBtn.classList.add('btn'); + filterBtn.innerHTML = 'Show all'; + filterBtn.addEventListener('click', () => { + ownfilter = !ownfilter; + filterBtn.classList.toggle('active'); + filterBtn.innerHTML = ownfilter + ? 'Show own' + : 'Show all'; + update(); + }); + + + update(); + }; +})(); diff --git a/webroot/js/socket.js b/webroot/js/socket.js index 4219bb7..cc13f0d 100644 --- a/webroot/js/socket.js +++ b/webroot/js/socket.js @@ -35,6 +35,11 @@ let socket = {'readyState': 0}; store.stats = msg.body; } break; + case 'cmd': + if (msg.body) { + store.updateCMD(msg.body); + } + break; default: notify.send('warn', `unable to identify message: ${raw}`); break; @@ -57,10 +62,18 @@ let socket = {'readyState': 0}; 'type': 'system' }); + socket.send(socketMsg); + notify.send('success', notifyMsg); + } + + function sendcmd (cmd) { + const notifyMsg = `Befehl '${cmd.cmd}' wird überall ausgeführt.`, + socketMsg = JSON.stringify({ + 'body': cmd, + 'type': 'cmd' + }); socket.send(socketMsg); - - notify.send('success', notifyMsg); } @@ -71,6 +84,7 @@ let socket = {'readyState': 0}; socket.onmessage = onmessage; socket.onclose = onclose; socket.sendnode = sendnode; + socket.sendcmd = sendcmd; } connect(); diff --git a/webroot/js/store.js b/webroot/js/store.js index 9f6d581..bd53f24 100644 --- a/webroot/js/store.js +++ b/webroot/js/store.js @@ -18,7 +18,8 @@ const store = { 'use strict'; const current = {}, - list = {}; + list = {}, + cmds = {}; function getNode (nodeid) { let node = {}; @@ -51,9 +52,18 @@ const store = { } }; + store.getNode = getNode; store.getNodes = function getNodes () { return Object.keys(list).map(getNode); }; + + store.updateCMD = function updateCMD (cmd) { + cmds[cmd.id] = cmd; + }; + + store.getCMDs = function getCMDs () { + return cmds; + }; })(); diff --git a/websocket/msg.go b/websocket/msg.go index b655f29..c5b77b9 100644 --- a/websocket/msg.go +++ b/websocket/msg.go @@ -12,4 +12,5 @@ const ( MessageTypeSystemNode = "system" MessageTypeCurrentNode = "current" MessageTypeStats = "stats" + MessageTypeCmd = "cmd" )