[TASK] nice list

This commit is contained in:
Martin Geno 2017-05-12 21:32:10 +02:00
parent ac87050fa0
commit b190bd43c4
No known key found for this signature in database
GPG Key ID: F0D39A37E925E941
12 changed files with 241 additions and 40 deletions

View File

@ -7,6 +7,6 @@ ssh_key = "/etc/id_rsa"
ssh_interface = "wlp4s0" ssh_interface = "wlp4s0"
[yanic] [yanic]
enable = true enable = false
type = "unix" type = "unix"
address = "/tmp/yanic-database.socket" address = "/tmp/yanic-database.socket"

View File

@ -4,10 +4,10 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"time"
"github.com/FreifunkBremen/freifunkmanager/ssh" "github.com/FreifunkBremen/freifunkmanager/ssh"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/jsontime"
yanicRuntime "github.com/FreifunkBremen/yanic/runtime" yanicRuntime "github.com/FreifunkBremen/yanic/runtime"
) )
@ -15,10 +15,11 @@ const (
SSHUpdateHostname = "uci set system.@system[0].hostname='%s';uci commit system;echo $(uci get system.@system[0].hostname) > /proc/sys/kernel/hostname" SSHUpdateHostname = "uci set system.@system[0].hostname='%s';uci commit system;echo $(uci get system.@system[0].hostname) > /proc/sys/kernel/hostname"
SSHUpdateOwner = "uci set gluon-node-info.@owner[0].contact='%s';uci commit gluon-node-info;" SSHUpdateOwner = "uci set gluon-node-info.@owner[0].contact='%s';uci commit gluon-node-info;"
SSHUpdateLocation = "uci set gluon-node-info.@location[0].latitude='%d';uci set gluon-node-info.@location[0].longitude='%d';uci set gluon-node-info.@location[0].share_location=1;uci commit gluon-node-info;" SSHUpdateLocation = "uci set gluon-node-info.@location[0].latitude='%d';uci set gluon-node-info.@location[0].longitude='%d';uci set gluon-node-info.@location[0].share_location=1;uci commit gluon-node-info;"
SSHUpdateWifiFreq = "uci set gluon-node-info.@location[0].latitude='%d';uci set gluon-node-info.@location[0].longitude='%d';uci set gluon-node-info.@location[0].share_location=1;uci commit gluon-node-info;wifi"
) )
type Node struct { type Node struct {
Lastseen time.Time `json:"lastseen"` Lastseen jsontime.Time `json:"lastseen"`
NodeID string `json:"node_id"` NodeID string `json:"node_id"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
Location data.Location `json:"location"` Location data.Location `json:"location"`

View File

@ -3,8 +3,9 @@ package runtime
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"time" "sync"
"github.com/FreifunkBremen/yanic/jsontime"
yanic "github.com/FreifunkBremen/yanic/runtime" yanic "github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/freifunkmanager/lib/log" "github.com/FreifunkBremen/freifunkmanager/lib/log"
@ -18,6 +19,7 @@ type Nodes struct {
statePath string statePath string
iface string iface string
notifyFunc []func(*Node, bool) notifyFunc []func(*Node, bool)
sync.Mutex
} }
func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes { func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes {
@ -37,13 +39,15 @@ func (nodes *Nodes) AddNode(n *yanic.Node) {
if node == nil { if node == nil {
return return
} }
node.Lastseen = jsontime.Now()
logger := log.Log.WithField("method", "AddNode").WithField("node_id", node.NodeID) logger := log.Log.WithField("method", "AddNode").WithField("node_id", node.NodeID)
nodes.Lock()
nodes.Unlock()
if cNode := nodes.List[node.NodeID]; cNode != nil { if cNode := nodes.List[node.NodeID]; cNode != nil {
cNode.Lastseen = time.Now() cNode.Lastseen = jsontime.Now()
cNode.Stats = node.Stats cNode.Stats = node.Stats
if uNode, ok := nodes.ToUpdate[node.NodeID]; ok { if uNode, ok := nodes.ToUpdate[node.NodeID]; ok {
uNode.Lastseen = time.Now() uNode.Lastseen = jsontime.Now()
uNode.Stats = node.Stats uNode.Stats = node.Stats
if nodes.List[node.NodeID].IsEqual(node) { if nodes.List[node.NodeID].IsEqual(node) {
delete(nodes.ToUpdate, node.NodeID) delete(nodes.ToUpdate, node.NodeID)
@ -59,7 +63,6 @@ func (nodes *Nodes) AddNode(n *yanic.Node) {
logger.Debugf("know already these node") logger.Debugf("know already these node")
return return
} }
node.Lastseen = time.Now()
// session := nodes.ssh.ConnectTo(node.Address) // session := nodes.ssh.ConnectTo(node.Address)
result, err := nodes.ssh.RunOn(node.GetAddress(nodes.iface), "uptime") result, err := nodes.ssh.RunOn(node.GetAddress(nodes.iface), "uptime")
if err != nil { if err != nil {
@ -87,6 +90,8 @@ func (nodes *Nodes) UpdateNode(node *Node) {
log.Log.Warn("no new node to update") log.Log.Warn("no new node to update")
return return
} }
nodes.Lock()
defer nodes.Unlock()
if n, ok := nodes.List[node.NodeID]; ok { if n, ok := nodes.List[node.NodeID]; ok {
go node.SSHUpdate(nodes.ssh, nodes.iface, n) go node.SSHUpdate(nodes.ssh, nodes.iface, n)
} }
@ -95,6 +100,8 @@ func (nodes *Nodes) UpdateNode(node *Node) {
} }
func (nodes *Nodes) Updater() { func (nodes *Nodes) Updater() {
nodes.Lock()
defer nodes.Unlock()
for nodeid := range nodes.ToUpdate { for nodeid := range nodes.ToUpdate {
if node := nodes.List[nodeid]; node != nil { if node := nodes.List[nodeid]; node != nil {
go node.SSHSet(nodes.ssh, nodes.iface) go node.SSHSet(nodes.ssh, nodes.iface)
@ -116,6 +123,8 @@ func (nodes *Nodes) load() {
} }
func (nodes *Nodes) Saver() { func (nodes *Nodes) Saver() {
nodes.Lock()
yanic.SaveJSON(nodes, nodes.statePath) yanic.SaveJSON(nodes, nodes.statePath)
nodes.Unlock()
log.Log.Debug("saved state file") log.Log.Debug("saved state file")
} }

View File

@ -104,23 +104,54 @@ thead {
thead tr th{ thead tr th{
border-bottom: 4px solid #dc0067; border-bottom: 4px solid #dc0067;
} }
table th > input {
table th.sort-down:after { border: none;
content: " \25BE" color: #000;
font-weight: bold;
background: #fff;
} }
table th.sort-up:after { table th.sortable.sort-down:after {
content: " \25B4" content: " \25BC"
}
table th.sortable.sort-up:after {
content: " \25B2"
}
table th.sortable:not(.sort-down):not(.sort-up):after {
content: " \25B4\25BE";
}
table.nodes {
width: 100%;
} }
button { table.nodes td > span{
display: inline-block; display: block;
padding: .5em 1em;
border-radius: 2em;
color: #fff;
background-color: #dc0067;
text-align: center;
} }
button:hover { table.nodes tbody tr:nth-child(even) {
background: #eee;
}
table.nodes tbody tr:nth-child(odd) {
background: #fff;
}
table.nodes tbody tr:hover {
background: #ccc;
}
table.nodes tbody tr.offline{
background: #ffb400;
}
table.nodes tbody tr.offline:hover{
background: #dc0067;
}
.btn {
display: inline-block;
padding: .3em .5em;
border-radius: 1em;
color: #fff;
background-color: #dc0067;
text-align: center;
cursor: pointer;
}
.btn:hover {
background: lighten(#dc0067, 5%); background: lighten(#dc0067, 5%);
} }
@ -128,6 +159,6 @@ a.btn:hover {
text-decoration: none; text-decoration: none;
} }
a { a {
color: #dc0067; color: #dc0067;
text-decoration: none; text-decoration: none;
} }

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>FreifunkManager</title> <title>FreifunkManager</title>
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<script src="/js/moment.js"></script>
<script src="/js/navigo.js"></script> <script src="/js/navigo.js"></script>
<script src="/js/config.js"></script> <script src="/js/config.js"></script>
<script src="/js/domlib.js"></script> <script src="/js/domlib.js"></script>

View File

@ -1,4 +1,4 @@
var config = { var config = {
title: 'FreifunkManager - Breminale', title: 'FreifunkManager - Breminale',
backend: 'ws://localhost:8080/websocket' backend: 'ws://'+location.host+'/websocket'
}; };

View File

@ -8,18 +8,15 @@ var router = new Navigo(null, true, '#');
router.on({ router.on({
'/list': function () { '/list': function () {
clean(); clean();
console.log("list view");
guiList.bind(document.querySelector('main')); guiList.bind(document.querySelector('main'));
guiList.render(); guiList.render();
}, },
'/map':function(){ '/map':function(){
clean(); clean();
console.log("map view");
domlib.newAt(main,"div").innerHTML = "Map"; domlib.newAt(main,"div").innerHTML = "Map";
}, },
'/statistics':function(){ '/statistics':function(){
clean(); clean();
console.log("stats view");
domlib.newAt(document.querySelector('main'),"div").innerHTML = "Stats"; domlib.newAt(document.querySelector('main'),"div").innerHTML = "Stats";
}, },
'/n/:nodeID': { '/n/:nodeID': {

View File

@ -6,10 +6,41 @@ var guiList = {};
var sortReverse = false; var sortReverse = false;
var sortIndex; var sortIndex;
var hostnameFilter,nodeidFilter;
function sort(a,b){ function sort(a,b){
function sortNumber(a,b){
return a - b;
}
if(sortIndex === undefined) if(sortIndex === undefined)
return a.node_id.localeCompare(b.node_id); return a.node_id.localeCompare(b.node_id);
switch (sortIndex.innerHTML) { switch (sortIndex.innerHTML) {
case "Lastseen":
return a.lastseen - b.lastseen;
case "CurPower":
return a._wireless.txpower24 - b._wireless.txpower24;
case "Power":
return a.wireless.txpower24 - b.wireless.txpower24;
case "CurChannel":
return a._wireless.channel24 - b._wireless.channel24;
case "Channel":
return a.wireless.channel24 - b.wireless.channel24;
case "Clients":
return a.statistics.clients.wifi24 - b.statistics.clients.wifi24;
case "ChanUtil":
var aMax = a.statistics.wireless.map(function(d){
return d.ChanUtil
}).sort(sortNumber);
var bMax = b.statistics.wireless.map(function(d){
return d.ChanUtil
}).sort(sortNumber);
if(!sortReverse){
aMax = aMax.reverse();
bMax = bMax.reverse();
}
return bMax[0] - aMax[0];
case "Hostname": case "Hostname":
return a.hostname.localeCompare(b.hostname); return a.hostname.localeCompare(b.hostname);
default: default:
@ -19,13 +50,56 @@ var guiList = {};
function renderRow(data){ function renderRow(data){
var tr = document.createElement('tr'); var tr = document.createElement('tr');
var startdate = new Date();
startdate.setMinutes(startdate.getMinutes() - 1);
if(new Date(data.lastseen) < startdate)
tr.classList.add('offline')
var td; var td;
domlib.newAt(tr,'td').innerHTML = moment(data.lastseen).fromNow(true);
domlib.newAt(tr,'td').innerHTML = data.node_id; domlib.newAt(tr,'td').innerHTML = data.node_id;
var cell1 = domlib.newAt(tr,'td'); domlib.newAt(tr,'td').innerHTML = data.hostname;
cell1.innerHTML = data.hostname;
cell1.addEventListener('click',function(){ var freq = domlib.newAt(tr,'td');
domlib.newAt(freq,'span').innerHTML = '2.4 Ghz';
domlib.newAt(freq,'span').innerHTML = '5 Ghz';
var curchannel = domlib.newAt(tr,'td');
domlib.newAt(curchannel,'span').innerHTML = data._wireless.channel24||'-';
domlib.newAt(curchannel,'span').innerHTML = data._wireless.channel5||'-';
var channel = domlib.newAt(tr,'td');
domlib.newAt(channel,'span').innerHTML = data.wireless.channel24||'-';
domlib.newAt(channel,'span').innerHTML = data.wireless.channel5||'-';
var curpower = domlib.newAt(tr,'td');
domlib.newAt(curpower,'span').innerHTML = data._wireless.txpower24||'-';
domlib.newAt(curpower,'span').innerHTML = data._wireless.txpower5||'-';
var power = domlib.newAt(tr,'td');
domlib.newAt(power,'span').innerHTML = data.wireless.txpower24||'-';
domlib.newAt(power,'span').innerHTML = data.wireless.txpower5||'-';
var client = domlib.newAt(tr,'td');
domlib.newAt(client,'span').innerHTML = data.statistics.clients.wifi24;
domlib.newAt(client,'span').innerHTML = data.statistics.clients.wifi5;
var chanUtil = domlib.newAt(tr,'td');
var chanUtil24 = data.statistics.wireless.filter(function(d){
return d.frequency < 5000;
})[0];
var chanUtil5 = data.statistics.wireless.filter(function(d){
return d.frequency > 5000;
})[0];
domlib.newAt(chanUtil,'span').innerHTML = chanUtil24.ChanUtil||'-';
domlib.newAt(chanUtil,'span').innerHTML = chanUtil5.ChanUtil||'-';
var option = domlib.newAt(tr,'td');
edit = domlib.newAt(option,'div');
edit.classList.add('btn');
edit.innerHTML = 'Edit';
edit.addEventListener('click',function(){
router.navigate(router.generate('node', { nodeID: data.node_id })); router.navigate(router.generate('node', { nodeID: data.node_id }));
}); });
@ -36,10 +110,19 @@ var guiList = {};
domlib.removeChildren(tbody); domlib.removeChildren(tbody);
var data = store.will(); var data = store.will();
if(hostnameFilter && hostnameFilter.value != "")
data = data.filter(function(d){
return d.hostname.toLowerCase().indexOf(hostnameFilter.value) > -1;
})
if(nodeidFilter && nodeidFilter.value != "")
data = data.filter(function(d){
return d.node_id.indexOf(nodeidFilter.value) > -1;
})
data = data.sort(sort);
if(sortReverse) if(sortReverse)
data = data.reverse(sort); data = data.reverse();
else
data = data.sort(sort);
for(var i=0; i<data.length; i++){ for(var i=0; i<data.length; i++){
var row = renderRow(data[i]); var row = renderRow(data[i]);
@ -63,7 +146,6 @@ var guiList = {};
guiList.render = function render(){ guiList.render = function render(){
if (container === undefined){ if (container === undefined){
console.log("unable to render guiList");
return; return;
} else if (tbody !== undefined){ } else if (tbody !== undefined){
container.appendChild(tbody.parentNode); container.appendChild(tbody.parentNode);
@ -78,18 +160,73 @@ var guiList = {};
var tr = domlib.newAt(thead,'tr'); var tr = domlib.newAt(thead,'tr');
var cell1 = domlib.newAt(tr,'th'); var cell1 = domlib.newAt(tr,'th');
cell1.innerHTML = "NodeID"; cell1.innerHTML = "Lastseen";
cell1.addEventListener('click', function(){ cell1.addEventListener('click', function(){
sortTable(cell1); sortTable(cell1);
}); });
var cell2 = domlib.newAt(tr,'th'); var cell2 = domlib.newAt(tr,'th');
cell2.innerHTML = "Hostname"; cell2.classList.add('sortable');
cell2.addEventListener('click', function(){ nodeidFilter = domlib.newAt(cell2,'input');
nodeidFilter.setAttribute("placeholder","NodeID");
nodeidFilter.setAttribute("size","9");
nodeidFilter.addEventListener('keyup', updateTable);
cell2.addEventListener('dblclick', function(){
sortTable(cell2); sortTable(cell2);
}); });
table.classList.add('sorttable'); var cell3 = domlib.newAt(tr,'th');
cell3.classList.add('sortable');
hostnameFilter = domlib.newAt(cell3,'input');
hostnameFilter.setAttribute("placeholder","Hostname");
hostnameFilter.addEventListener('keyup', updateTable);
cell3.addEventListener('dblclick', function(){
sortTable(cell3);
});
domlib.newAt(tr,'th').innerHTML = 'Freq';
var cell4 = domlib.newAt(tr,'th');
cell4.innerHTML = "CurChannel";
cell4.classList.add('sortable');
cell4.addEventListener('click', function(){
sortTable(cell4);
});
var cell5 = domlib.newAt(tr,'th');
cell5.innerHTML = "Channel";
cell5.classList.add('sortable');
cell5.addEventListener('click', function(){
sortTable(cell5);
});
var cell6 = domlib.newAt(tr,'th');
cell6.innerHTML = "CurPower";
cell6.classList.add('sortable');
cell6.addEventListener('click', function(){
sortTable(cell6);
});
var cell7 = domlib.newAt(tr,'th');
cell7.innerHTML = "Power";
cell7.classList.add('sortable');
cell7.addEventListener('click', function(){
sortTable(cell7);
});
var cell8 = domlib.newAt(tr,'th');
cell8.innerHTML = "Clients";
cell8.classList.add('sortable');
cell8.addEventListener('click', function(){
sortTable(cell8);
});
var cell9 = domlib.newAt(tr,'th');
cell9.innerHTML = "ChanUtil";
cell9.classList.add('sortable');
cell9.addEventListener('click', function(){
sortTable(cell9);
});
domlib.newAt(tr,'th').innerHTML = "Option";
table.classList.add('nodes');
updateTable(); updateTable();
}; };

7
webroot/js/moment.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,14 @@ var store = {
}; };
store.will = function() { store.will = function() {
return Object.keys(store.list).map(function(nodeid){ return Object.keys(store.list).map(function(nodeid){
var node;
if (store.toupdate[nodeid]) { if (store.toupdate[nodeid]) {
return store.toupdate[nodeid]; node = store.toupdate[nodeid];
} else{
node = store.list[nodeid];
} }
return store.list[nodeid]; node._wireless = store.list[nodeid].wireless;
return node;
}); });
}; };
})(); })();

View File

@ -37,7 +37,9 @@ func (c *Client) Write(msg *Message) {
select { select {
case c.ch <- msg: case c.ch <- msg:
default: default:
clientsMutex.Lock()
delete(clients, c.ip) delete(clients, c.ip)
clientsMutex.Unlock()
log.HTTP(c.ws.Request()).Error("client disconnected") log.HTTP(c.ws.Request()).Error("client disconnected")
} }
} }
@ -81,7 +83,9 @@ func (c *Client) listenWrite() {
case <-c.writeQuit: case <-c.writeQuit:
close(c.ch) close(c.ch)
close(c.writeQuit) close(c.writeQuit)
clientsMutex.Lock()
delete(clients, c.ip) delete(clients, c.ip)
clientsMutex.Unlock()
return return
} }
} }
@ -94,7 +98,9 @@ func (c *Client) listenRead() {
case <-c.readQuit: case <-c.readQuit:
close(c.readQuit) close(c.readQuit)
clientsMutex.Lock()
delete(clients, c.ip) delete(clients, c.ip)
clientsMutex.Unlock()
return return
default: default:

View File

@ -2,6 +2,7 @@ package websocket
import ( import (
"net/http" "net/http"
"sync"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
@ -12,6 +13,7 @@ import (
var nodes *runtime.Nodes var nodes *runtime.Nodes
var clients map[string]*Client var clients map[string]*Client
var clientsMutex sync.Mutex
func Start(nodeBind *runtime.Nodes) { func Start(nodeBind *runtime.Nodes) {
nodes = nodeBind nodes = nodeBind
@ -23,14 +25,18 @@ func Start(nodeBind *runtime.Nodes) {
defer func() { defer func() {
ws.Close() ws.Close()
clientsMutex.Lock()
delete(clients, ip) delete(clients, ip)
clientsMutex.Unlock()
log.HTTP(r).Info("client disconnected") log.HTTP(r).Info("client disconnected")
}() }()
log.HTTP(r).Infof("new client") log.HTTP(r).Infof("new client")
client := NewClient(ip, ws) client := NewClient(ip, ws)
clientsMutex.Lock()
clients[ip] = client clients[ip] = client
clientsMutex.Unlock()
client.Listen() client.Listen()
})) }))
@ -43,9 +49,11 @@ func Notify(node *runtime.Node, real bool) {
if real { if real {
msgType = MessageTypeCurrentNode msgType = MessageTypeCurrentNode
} }
clientsMutex.Lock()
for _, c := range clients { for _, c := range clients {
c.Write(&Message{Type: msgType, Node: node}) c.Write(&Message{Type: msgType, Node: node})
} }
clientsMutex.Unlock()
} }
func Close() { func Close() {