[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"
[yanic]
enable = true
enable = false
type = "unix"
address = "/tmp/yanic-database.socket"

View File

@ -4,10 +4,10 @@ import (
"bytes"
"fmt"
"net"
"time"
"github.com/FreifunkBremen/freifunkmanager/ssh"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/jsontime"
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"
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;"
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 {
Lastseen time.Time `json:"lastseen"`
Lastseen jsontime.Time `json:"lastseen"`
NodeID string `json:"node_id"`
Hostname string `json:"hostname"`
Location data.Location `json:"location"`

View File

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

View File

@ -104,23 +104,54 @@ thead {
thead tr th{
border-bottom: 4px solid #dc0067;
}
table th.sort-down:after {
content: " \25BE"
table th > input {
border: none;
color: #000;
font-weight: bold;
background: #fff;
}
table th.sort-up:after {
content: " \25B4"
table th.sortable.sort-down:after {
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: block;
}
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: .5em 1em;
border-radius: 2em;
padding: .3em .5em;
border-radius: 1em;
color: #fff;
background-color: #dc0067;
text-align: center;
cursor: pointer;
}
button:hover {
.btn:hover {
background: lighten(#dc0067, 5%);
}

View File

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

View File

@ -1,4 +1,4 @@
var config = {
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({
'/list': function () {
clean();
console.log("list view");
guiList.bind(document.querySelector('main'));
guiList.render();
},
'/map':function(){
clean();
console.log("map view");
domlib.newAt(main,"div").innerHTML = "Map";
},
'/statistics':function(){
clean();
console.log("stats view");
domlib.newAt(document.querySelector('main'),"div").innerHTML = "Stats";
},
'/n/:nodeID': {

View File

@ -6,10 +6,41 @@ var guiList = {};
var sortReverse = false;
var sortIndex;
var hostnameFilter,nodeidFilter;
function sort(a,b){
function sortNumber(a,b){
return a - b;
}
if(sortIndex === undefined)
return a.node_id.localeCompare(b.node_id);
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":
return a.hostname.localeCompare(b.hostname);
default:
@ -19,13 +50,56 @@ var guiList = {};
function renderRow(data){
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;
domlib.newAt(tr,'td').innerHTML = moment(data.lastseen).fromNow(true);
domlib.newAt(tr,'td').innerHTML = data.node_id;
var cell1 = domlib.newAt(tr,'td');
cell1.innerHTML = data.hostname;
cell1.addEventListener('click',function(){
domlib.newAt(tr,'td').innerHTML = data.hostname;
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 }));
});
@ -36,11 +110,20 @@ var guiList = {};
domlib.removeChildren(tbody);
var data = store.will();
if(sortReverse)
data = data.reverse(sort);
else
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)
data = data.reverse();
for(var i=0; i<data.length; i++){
var row = renderRow(data[i]);
tbody.appendChild(row);
@ -63,7 +146,6 @@ var guiList = {};
guiList.render = function render(){
if (container === undefined){
console.log("unable to render guiList");
return;
} else if (tbody !== undefined){
container.appendChild(tbody.parentNode);
@ -78,18 +160,73 @@ var guiList = {};
var tr = domlib.newAt(thead,'tr');
var cell1 = domlib.newAt(tr,'th');
cell1.innerHTML = "NodeID";
cell1.innerHTML = "Lastseen";
cell1.addEventListener('click', function(){
sortTable(cell1);
});
var cell2 = domlib.newAt(tr,'th');
cell2.innerHTML = "Hostname";
cell2.addEventListener('click', function(){
cell2.classList.add('sortable');
nodeidFilter = domlib.newAt(cell2,'input');
nodeidFilter.setAttribute("placeholder","NodeID");
nodeidFilter.setAttribute("size","9");
nodeidFilter.addEventListener('keyup', updateTable);
cell2.addEventListener('dblclick', function(){
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();
};

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() {
return Object.keys(store.list).map(function(nodeid){
var node;
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 {
case c.ch <- msg:
default:
clientsMutex.Lock()
delete(clients, c.ip)
clientsMutex.Unlock()
log.HTTP(c.ws.Request()).Error("client disconnected")
}
}
@ -81,7 +83,9 @@ func (c *Client) listenWrite() {
case <-c.writeQuit:
close(c.ch)
close(c.writeQuit)
clientsMutex.Lock()
delete(clients, c.ip)
clientsMutex.Unlock()
return
}
}
@ -94,7 +98,9 @@ func (c *Client) listenRead() {
case <-c.readQuit:
close(c.readQuit)
clientsMutex.Lock()
delete(clients, c.ip)
clientsMutex.Unlock()
return
default:

View File

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