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"
)