cleanup and migrate to newer libraries (use NEW Javascript env) + add secret

This commit is contained in:
Martin/Geno 2018-06-30 16:20:54 +02:00
parent a86ac56b75
commit 82270e6fa3
No known key found for this signature in database
GPG Key ID: 9D7D3C6BFF600C6A
62 changed files with 7101 additions and 3148 deletions

6
.gitignore vendored
View File

@ -124,12 +124,6 @@ __pycache__
# IDE's go # IDE's go
.idea/ .idea/
# webroot
webroot/node_modules
webroot/data
webroot/app.js
webroot/app.js.map
# go project # go project
profile.cov profile.cov
config.conf config.conf

View File

@ -3,6 +3,7 @@ state_path = "/tmp/freifunkmanager.json"
webserver_bind = ":8080" webserver_bind = ":8080"
webroot = "./webroot" webroot = "./webroot"
secret = "passw0rd"
ssh_key = "~/.ssh/id_rsa" ssh_key = "~/.ssh/id_rsa"
ssh_interface = "wlp4s0" ssh_interface = "wlp4s0"

11
main.go
View File

@ -54,8 +54,9 @@ func main() {
go nodesSaveWorker.Start() go nodesSaveWorker.Start()
go nodesUpdateWorker.Start() go nodesUpdateWorker.Start()
websocket.Start(nodes) ws := websocket.NewWebsocketServer(config.Secret, nodes)
db.NotifyStats = websocket.NotifyStats nodes.AddNotifyStats(ws.SendStats)
nodes.AddNotifyNode(ws.SendNode)
if config.Yanic.Enable { if config.Yanic.Enable {
collector = respondYanic.NewCollector(db, nodesYanic, make(map[string][]string), []respondYanic.InterfaceConfig{respondYanic.InterfaceConfig{ collector = respondYanic.NewCollector(db, nodesYanic, make(map[string][]string), []respondYanic.InterfaceConfig{respondYanic.InterfaceConfig{
@ -71,7 +72,7 @@ func main() {
httpLib.Write(w, nodes) httpLib.Write(w, nodes)
}) })
http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
httpLib.Write(w, db.Statistics) httpLib.Write(w, nodes.Statistics)
}) })
http.Handle("/", gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot)))) http.Handle("/", gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot))))
@ -80,7 +81,7 @@ func main() {
} }
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Panic(err) log.Panic(err)
} }
}() }()
@ -92,7 +93,7 @@ func main() {
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigs sig := <-sigs
websocket.Close() ws.Close()
// Stop services // Stop services
srv.Close() srv.Close()

View File

@ -10,6 +10,9 @@ type Config struct {
// path to deliver static content // path to deliver static content
Webroot string `toml:"webroot"` Webroot string `toml:"webroot"`
// auth secret
Secret string `toml:"secret"`
// SSH private key // SSH private key
SSHPrivateKey string `toml:"ssh_key"` SSHPrivateKey string `toml:"ssh_key"`
SSHInterface string `toml:"ssh_interface"` SSHInterface string `toml:"ssh_interface"`

View File

@ -2,38 +2,25 @@ package runtime
import ( import (
"bytes" "bytes"
"fmt"
"net" "net"
log "github.com/sirupsen/logrus"
yanicData "github.com/FreifunkBremen/yanic/data" yanicData "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/lib/jsontime" "github.com/FreifunkBremen/yanic/lib/jsontime"
yanicRuntime "github.com/FreifunkBremen/yanic/runtime" yanicRuntime "github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/freifunkmanager/ssh"
)
const (
SSHUpdateHostname = "uci set system.@system[0].hostname='%s'; uci set wireless.priv_radio0.ssid=\"offline-$(uci get system.@system[0].hostname)\"; uci set wireless.priv_radio1.ssid=\"offline-$(uci get system.@system[0].hostname)\"; uci commit; echo $(uci get system.@system[0].hostname) > /proc/sys/kernel/hostname; wifi"
SSHUpdateOwner = "uci set gluon-node-info.@owner[0].contact='%s';uci commit gluon-node-info;"
SSHUpdateLocation = "uci set gluon-node-info.@location[0].latitude='%f';uci set gluon-node-info.@location[0].longitude='%f';uci set gluon-node-info.@location[0].share_location=1;uci commit gluon-node-info;"
SSHUpdateWifiFreq24 = "if [ \"$(uci get wireless.radio0.hwmode | grep -c g)\" -ne 0 ]; then uci set wireless.radio0.channel='%d'; uci set wireless.radio0.txpower='%d'; elif [ \"$(uci get wireless.radio1.hwmode | grep -c g)\" -ne 0 ]; then uci set wireless.radio1.channel='%d'; uci set wireless.radio1.txpower='%d'; fi;"
SSHUpdateWifiFreq5 = "if [ \"$(uci get wireless.radio0.hwmode | grep -c a)\" -ne 0 ]; then uci set wireless.radio0.channel='%d'; uci set wireless.radio0.txpower='%d'; elif [ \"$(uci get wireless.radio1.hwmode | grep -c a)\" -ne 0 ]; then uci set wireless.radio1.channel='%d'; uci set wireless.radio1.txpower='%d'; fi;"
) )
type Node struct { type Node struct {
Lastseen jsontime.Time `json:"lastseen"` Lastseen jsontime.Time `json:"lastseen" mapstructure:"-"`
NodeID string `json:"node_id"` NodeID string `json:"node_id" mapstructure:"node_id"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
Location yanicData.Location `json:"location"` Location yanicData.Location `json:"location"`
Wireless yanicData.Wireless `json:"wireless"` Wireless yanicData.Wireless `json:"wireless"`
Owner string `json:"owner"` Owner string `json:"owner"`
Address net.IP `json:"-"` Address net.IP `json:"ip" mapstructure:"-"`
Stats struct { Stats struct {
Wireless yanicData.WirelessStatistics `json:"wireless"` Wireless yanicData.WirelessStatistics `json:"wireless"`
Clients yanicData.Clients `json:"clients"` Clients yanicData.Clients `json:"clients"`
} `json:"statistics"` } `json:"statistics" mapstructure:"-"`
} }
func NewNode(nodeOrigin *yanicRuntime.Node) *Node { func NewNode(nodeOrigin *yanicRuntime.Node) *Node {
@ -65,28 +52,6 @@ func NewNode(nodeOrigin *yanicRuntime.Node) *Node {
} }
return nil return nil
} }
func (n *Node) SSHUpdate(ssh *ssh.Manager, iface string, oldnode *Node) {
addr := n.GetAddress(iface)
if oldnode == nil || n.Hostname != oldnode.Hostname {
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateHostname, n.Hostname))
}
if oldnode == nil || n.Owner != oldnode.Owner {
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateOwner, n.Owner))
}
if oldnode == nil || !locationEqual(n.Location, oldnode.Location) {
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateLocation, n.Location.Latitude, n.Location.Longitude))
}
if oldnode == nil || !wirelessEqual(n.Wireless, oldnode.Wireless) {
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateWifiFreq24, n.Wireless.Channel24, n.Wireless.TxPower24, n.Wireless.Channel24, n.Wireless.TxPower24))
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateWifiFreq5, n.Wireless.Channel5, n.Wireless.TxPower5, n.Wireless.Channel5, n.Wireless.TxPower5))
ssh.ExecuteOn(addr, "wifi")
// send warning for running wifi, because it kicks clients from node
log.Warn("[cmd] wifi ", n.NodeID)
}
oldnode = n
}
func (n *Node) GetAddress(iface string) net.TCPAddr { func (n *Node) GetAddress(iface string) net.TCPAddr {
return net.TCPAddr{IP: n.Address, Port: 22, Zone: iface} return net.TCPAddr{IP: n.Address, Port: 22, Zone: iface}
} }

105
runtime/node_ssh.go Normal file
View File

@ -0,0 +1,105 @@
package runtime
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/FreifunkBremen/freifunkmanager/ssh"
)
func (n *Node) SSHUpdate(sshmgmt *ssh.Manager, iface string, oldnode *Node) {
addr := n.GetAddress(iface)
client, err := sshmgmt.ConnectTo(addr)
if err != nil {
return
}
defer client.Close()
if oldnode == nil || n.Hostname != oldnode.Hostname {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set system.@system[0].hostname='%s';
uci set wireless.priv_radio0.ssid="offline-$(uci get system.@system[0].hostname)";
uci set wireless.priv_radio1.ssid="offline-$(uci get system.@system[0].hostname)";
uci commit; echo $(uci get system.@system[0].hostname) > /proc/sys/kernel/hostname;`,
n.Hostname))
}
if oldnode == nil || n.Owner != oldnode.Owner {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set gluon-node-info.@owner[0].contact='%s';
uci commit gluon-node-info;`,
n.Owner))
}
if oldnode == nil || !locationEqual(n.Location, oldnode.Location) {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set gluon-node-info.@location[0].latitude='%f';
uci set gluon-node-info.@location[0].longitude='%f';
uci set gluon-node-info.@location[0].share_location=1;
uci commit gluon-node-info;`,
n.Location.Latitude, n.Location.Longitude))
}
runWifi := false
defer func() {
if runWifi {
ssh.Execute(n.Address.String(), client, "wifi")
// send warning for running wifi, because it kicks clients from node
log.Warn("[cmd] wifi ", n.NodeID)
}
}()
result, err := ssh.Run(n.Address.String(), client, `
if [ "$(uci get wireless.radio0.hwmode | grep -c g)" -ne 0 ]; then
echo "radio0";
elif [ "$(uci get wireless.radio1.hwmode | grep -c g)" -ne 0 ]; then
echo "radio1";
fi;`)
if err != nil {
return
}
radio := ssh.SSHResultToString(result)
if radio != "" {
if oldnode == nil || n.Wireless.TxPower24 != oldnode.Wireless.TxPower24 {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set wireless.%s.txpower='%d';
uci commit wireless;`,
radio, n.Wireless.TxPower24))
runWifi = true
}
if oldnode == nil || n.Wireless.Channel24 != oldnode.Wireless.Channel24 {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set wireless.%s.channel='%d';
uci commit wireless;`,
radio, n.Wireless.Channel24))
runWifi = true
}
}
result, err = ssh.Run(n.Address.String(), client, `
if [ "$(uci get wireless.radio0.hwmode | grep -c a)" -ne 0 ]; then
echo "radio0";
elif [ "$(uci get wireless.radio1.hwmode | grep -c a)" -ne 0 ]; then
echo "radio1";
fi;`)
if err != nil {
return
}
radio = ssh.SSHResultToString(result)
if radio != "" {
if oldnode == nil || n.Wireless.TxPower5 != oldnode.Wireless.TxPower5 {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set wireless.%s.txpower='%d';
uci commit wireless;`,
radio, n.Wireless.TxPower5))
runWifi = true
}
if oldnode == nil || n.Wireless.Channel5 != oldnode.Wireless.Channel5 {
ssh.Execute(n.Address.String(), client, fmt.Sprintf(`
uci set wireless.%s.channel='%d';
uci commit wireless;`,
radio, n.Wireless.Channel5))
runWifi = true
}
}
oldnode = n
}

View File

@ -3,10 +3,9 @@ package runtime
import ( import (
"sync" "sync"
"github.com/FreifunkBremen/yanic/lib/jsontime"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
yanicRuntime "github.com/FreifunkBremen/yanic/runtime" runtimeYanic "github.com/FreifunkBremen/yanic/runtime"
"github.com/genofire/golang-lib/file" "github.com/genofire/golang-lib/file"
"github.com/FreifunkBremen/freifunkmanager/ssh" "github.com/FreifunkBremen/freifunkmanager/ssh"
@ -15,10 +14,12 @@ import (
type Nodes struct { type Nodes struct {
List map[string]*Node `json:"nodes"` List map[string]*Node `json:"nodes"`
Current map[string]*Node `json:"-"` Current map[string]*Node `json:"-"`
Statistics *runtimeYanic.GlobalStats `json:"-"`
ssh *ssh.Manager ssh *ssh.Manager
statePath string statePath string
iface string iface string
notifyFunc []func(*Node, bool) notifyNodeFunc []func(*Node, bool)
notifyStatsFunc []func(*runtimeYanic.GlobalStats)
sync.Mutex sync.Mutex
} }
@ -34,53 +35,25 @@ func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes {
return nodes return nodes
} }
func (nodes *Nodes) LearnNode(n *yanicRuntime.Node) { func (nodes *Nodes) AddNotifyNode(f func(*Node, bool)) {
node := NewNode(n) nodes.notifyNodeFunc = append(nodes.notifyNodeFunc, f)
if node == nil {
return
}
node.Lastseen = jsontime.Now()
logger := log.WithField("method", "LearnNode").WithField("node_id", node.NodeID)
nodes.Lock()
defer nodes.Unlock()
if lNode := nodes.List[node.NodeID]; lNode != nil {
lNode.Lastseen = jsontime.Now()
lNode.Stats = node.Stats
} else {
nodes.List[node.NodeID] = node
nodes.notify(node, true)
}
if _, ok := nodes.Current[node.NodeID]; ok {
nodes.Current[node.NodeID] = node
nodes.notify(node, false)
return
}
// session := nodes.ssh.ConnectTo(node.Address)
result, err := nodes.ssh.RunOn(node.GetAddress(nodes.iface), "uptime")
if err != nil {
logger.Debug("init ssh command not run", err)
return
}
uptime := ssh.SSHResultToString(result)
logger.Infof("new node with uptime: %s", uptime)
nodes.Current[node.NodeID] = node
if lNode := nodes.List[node.NodeID]; lNode != nil {
lNode.Address = node.Address
go lNode.SSHUpdate(nodes.ssh, nodes.iface, node)
}
nodes.notify(node, false)
} }
func (nodes *Nodes) notifyNode(node *Node, system bool) {
func (nodes *Nodes) AddNotify(f func(*Node, bool)) { for _, f := range nodes.notifyNodeFunc {
nodes.notifyFunc = append(nodes.notifyFunc, f)
}
func (nodes *Nodes) notify(node *Node, system bool) {
for _, f := range nodes.notifyFunc {
f(node, system) f(node, system)
} }
} }
func (nodes *Nodes) AddNotifyStats(f func(stats *runtimeYanic.GlobalStats)) {
nodes.notifyStatsFunc = append(nodes.notifyStatsFunc, f)
}
func (nodes *Nodes) notifyStats(stats *runtimeYanic.GlobalStats) {
nodes.Statistics = stats
for _, f := range nodes.notifyStatsFunc {
f(stats)
}
}
func (nodes *Nodes) UpdateNode(node *Node) { func (nodes *Nodes) UpdateNode(node *Node) {
if node == nil { if node == nil {
log.Warn("no new node to update") log.Warn("no new node to update")
@ -94,7 +67,7 @@ func (nodes *Nodes) UpdateNode(node *Node) {
log.Info("update node", node.NodeID) log.Info("update node", node.NodeID)
} }
nodes.List[node.NodeID] = node nodes.List[node.NodeID] = node
nodes.notify(node, true) nodes.notifyNode(node, true)
} }
func (nodes *Nodes) Updater() { func (nodes *Nodes) Updater() {

View File

@ -3,15 +3,18 @@ package runtime
import ( import (
"time" "time"
log "github.com/sirupsen/logrus"
databaseYanic "github.com/FreifunkBremen/yanic/database" databaseYanic "github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/lib/jsontime"
runtimeYanic "github.com/FreifunkBremen/yanic/runtime" runtimeYanic "github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/freifunkmanager/ssh"
) )
type YanicDB struct { type YanicDB struct {
databaseYanic.Connection databaseYanic.Connection
nodes *Nodes nodes *Nodes
Statistics *runtimeYanic.GlobalStats
NotifyStats func(data *runtimeYanic.GlobalStats)
} }
func NewYanicDB(nodes *Nodes) *YanicDB { func NewYanicDB(nodes *Nodes) *YanicDB {
@ -20,18 +23,52 @@ func NewYanicDB(nodes *Nodes) *YanicDB {
} }
} }
func (conn *YanicDB) InsertNode(node *runtimeYanic.Node) { func (conn *YanicDB) InsertNode(n *runtimeYanic.Node) {
conn.nodes.LearnNode(node) node := NewNode(n)
if node == nil {
return
}
node.Lastseen = jsontime.Now()
logger := log.WithField("method", "LearnNode").WithField("node_id", node.NodeID)
conn.nodes.Lock()
defer conn.nodes.Unlock()
if lNode := conn.nodes.List[node.NodeID]; lNode != nil {
lNode.Lastseen = jsontime.Now()
lNode.Stats = node.Stats
} else {
conn.nodes.List[node.NodeID] = node
conn.nodes.notifyNode(node, true)
}
if _, ok := conn.nodes.Current[node.NodeID]; ok {
conn.nodes.Current[node.NodeID] = node
conn.nodes.notifyNode(node, false)
return
}
// session := nodes.ssh.ConnectTo(node.Address)
result, err := conn.nodes.ssh.RunOn(node.GetAddress(conn.nodes.iface), "uptime")
if err != nil {
logger.Debug("init ssh command not run", err)
return
}
uptime := ssh.SSHResultToString(result)
logger.Infof("new node with uptime: %s", uptime)
conn.nodes.Current[node.NodeID] = node
if lNode := conn.nodes.List[node.NodeID]; lNode != nil {
lNode.Address = node.Address
go lNode.SSHUpdate(conn.nodes.ssh, conn.nodes.iface, node)
}
conn.nodes.notifyNode(node, false)
} }
func (conn *YanicDB) InsertLink(link *runtimeYanic.Link, time time.Time) { func (conn *YanicDB) InsertLink(link *runtimeYanic.Link, time time.Time) {
} }
func (conn *YanicDB) InsertGlobals(stats *runtimeYanic.GlobalStats, time time.Time, site string, domain string) { func (conn *YanicDB) InsertGlobals(stats *runtimeYanic.GlobalStats, time time.Time, site string, domain string) {
conn.Statistics = stats if runtimeYanic.GLOBAL_SITE == site && runtimeYanic.GLOBAL_DOMAIN == domain {
if conn.NotifyStats != nil { conn.nodes.notifyStats(stats)
conn.NotifyStats(stats)
} }
} }
func (conn *YanicDB) PruneNodes(deleteAfter time.Duration) { func (conn *YanicDB) PruneNodes(deleteAfter time.Duration) {

View File

@ -14,10 +14,10 @@ func (m *Manager) ExecuteOn(addr net.TCPAddr, cmd string) error {
return err return err
} }
defer client.Close() defer client.Close()
return m.execute(addr.IP.String(), client, cmd) return Execute(addr.IP.String(), client, cmd)
} }
func (m *Manager) execute(host string, client *ssh.Client, cmd string) error { func Execute(host string, client *ssh.Client, cmd string) error {
session, err := client.NewSession() session, err := client.NewSession()
defer session.Close() defer session.Close()

View File

@ -29,10 +29,10 @@ func (m *Manager) RunOn(addr net.TCPAddr, cmd string) (string, error) {
return "", err return "", err
} }
defer client.Close() defer client.Close()
return m.run(addr.IP.String(), client, cmd) return Run(addr.IP.String(), client, cmd)
} }
func (m *Manager) run(host string, client *ssh.Client, cmd string) (string, error) { func Run(host string, client *ssh.Client, cmd string) (string, error) {
session, err := client.NewSession() session, err := client.NewSession()
defer session.Close() defer session.Close()

3
webroot/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["env"]
}

6
webroot/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
data
app.js
app.js.map
styles.css
styles.css.map

16
webroot/css/_login.less Normal file
View File

@ -0,0 +1,16 @@
nav .login {
float: right;
display: inline-block;
cursor: pointer;
text-decoration: none !important;
text-align: center;
box-sizing: border-box;
padding: 1em .5em;
height: 50px;
color: #fff;
a {
color: #fff;
padding-left: 2px;
}
}

61
webroot/css/_menu.less Normal file
View File

@ -0,0 +1,61 @@
header {
width: 100%;
height: 50px;
> div {
display: inline-block;
}
}
nav {
background-color: #373636;
position: fixed;
display: inline-block;
font-weight: 700;
width: 100%;
height: 50px;
ul {
padding: 0;
margin: 0;
list-style-type: none;
}
ul > li {
float:left;
display: inline-block;
a,
span {
display: inline-block;
cursor: pointer;
text-decoration: none !important;
text-align: center;
text-transform: uppercase;
color: inherit;
box-sizing: border-box;
padding: 1.1em .5em;
height: 50px;
}
&:hover,
&.active {
background: rgba(255, 255, 255, 0.2);
}
&.item-1 {
background: #ffb400;
color: #000;
}
&.item-2 {
background: #dc0067;
color: #fff;
}
&.item-3 {
background: #ccc;
color: #000;
}
}
}

29
webroot/css/_notify.less Normal file
View File

@ -0,0 +1,29 @@
.notifications {
position: absolute;
right: 1em;
}
.notify {
position: relative;
min-height: 1em;
margin: 1em 0;
padding: 1em 1.5em;
color: rgba(0,0,0,.87);
-webkit-transition: opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease;
transition: opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease;
box-shadow: 0 0 0 1px rgba(34,36,38,.22) inset, 0 0 0 0 transparent;
background: #ccc;
color: #000;
}
.notify.success {
background: #009ee0;
color: #fff;
}
.notify.warn {
background: #ffb400;
color: #000;
}
.notify.error {
background: #dc0067;
color: #fff;
}

25
webroot/css/_status.less Normal file
View File

@ -0,0 +1,25 @@
nav .status {
float: right;
background-color: #009ee0;
color: white;
width: 50px;
height: 50px;
&.connecting,
&.running {
background-color: #ffb400;
animation: blinkDot 2s infinite;
}
&.offline,
&.failed {
background-color: #dc0067;
animation: blinkDot 1s infinite;
}
@keyframes blinkDot {
50% {
background-color: rgba(255, 255, 255, 0.25);
}
}
}

View File

@ -1,62 +0,0 @@
.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;
font-size: 14px;
}
.console {
width: 100%;
border-spacing: 0px;
}
.console > tr {
clear: both;
border-spacing: 0px;
}
.console > tr > td {
width: 100%;
padding: 0px;
border-spacing: 0px;
height: 18px;
}
.console > tr.cmd > td {
margin-top: 3px;
}
.console > tr:not(.cmd) > td {
margin-top: 0px;
}
.console table,
.console table tr,
.console table td {
width: 100%;
padding: 0px;
margin: 0px;
border-collapse: collapse;
}
.console .time, .console .host{
color: #009ee0;
width: 1%;
}
.console .status {
text-align: right;
height: 18px;
}
.console table {
background-color: #ccc;
}
.console table .status {
width: 18px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

View File

@ -1,624 +0,0 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer {
max-width: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-control-zoom-out {
font-size: 20px;
}
.leaflet-touch .leaflet-control-zoom-in {
font-size: 22px;
}
.leaflet-touch .leaflet-control-zoom-out {
font-size: 24px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

View File

@ -1,3 +1,10 @@
@import "_notify.less";
@import "_menu.less";
@import "_status.less";
@import "_login.less";
@import "../node_modules/leaflet/dist/leaflet.css";
@import "_map.less";
body { body {
position: relative; position: relative;
margin: 0px; margin: 0px;
@ -6,20 +13,6 @@ body {
line-height: 1.3; line-height: 1.3;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
} }
.status {
float: right;
background: #009ee0;
color: white;
width: 50px;
height: 50px;
}
.status.connecting,.status.running {
background: #ffb400;
}
.status.offline, .status.failed {
background: #dc0067;
color: white;
}
span.online { span.online {
color: #009ee0; color: #009ee0;
} }
@ -29,85 +22,6 @@ span.offline {
h1 { h1 {
border-bottom: 4px solid #dc0067; border-bottom: 4px solid #dc0067;
} }
header {
width: 100%;
height: 50px;
}
header > div {
display: inline-block;
}
nav {
background-color: #373636;
position: fixed;
display: inline-block;
font-weight: 700;
width: 100%;
height: 50px;
}
nav ul {
padding: 0;
margin: 0;
list-style-type: none;
}
nav li {
float:left;
display: inline-block;
}
nav li:hover, nav.active {
background: rgba(255, 255, 255, 0.2);
}
nav li a, nav li span {
display: inline-block;
cursor: pointer;
text-decoration: none !important;
text-align: center;
text-transform: uppercase;
color: inherit;
box-sizing: border-box;
padding: 1.1em .5em;
height: 50px;
}
nav > ul > .item-1 {
background: #ffb400;
color: #000;
}
nav > ul > .item-2 {
background: #dc0067;
color: #fff;
}
nav > ul > .item-3 {
background: #ccc;
color: #000;
}
.notifications {
position: absolute;
right: 1em;
}
.notify {
position: relative;
min-height: 1em;
margin: 1em 0;
padding: 1em 1.5em;
color: rgba(0,0,0,.87);
-webkit-transition: opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease;
transition: opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease;
box-shadow: 0 0 0 1px rgba(34,36,38,.22) inset, 0 0 0 0 transparent;
background: #ccc;
color: #000;
}
.notify.success {
background: #009ee0;
color: #fff;
}
.notify.warn {
background: #ffb400;
color: #000;
}
.notify.error {
background: #dc0067;
color: #fff;
}
thead { thead {
font-size: 1.3em; font-size: 1.3em;

94
webroot/gulpfile.babel.js Normal file
View File

@ -0,0 +1,94 @@
import browserSync from 'browser-sync';
import browserify from 'browserify';
import buffer from 'vinyl-buffer';
import gulp from 'gulp';
import gulpLoadPlugins from 'gulp-load-plugins';
import source from 'vinyl-source-stream';
import sourcemaps from 'gulp-sourcemaps';
import watchify from 'watchify';
const gulpPlugins = gulpLoadPlugins();
function bundle (watching = false) {
const browserifyConf = {
'debug': true,
'entries': ['js/index.js'],
'transform': ['babelify']};
if (watching) {
browserifyConf.plugin = [watchify];
}
const browser = browserify(browserifyConf);
function bundler () {
return browser.bundle().
on('error', (err) => {
console.log(err.message);
}).
pipe(source('app.js')).
pipe(buffer()).
pipe(sourcemaps.init({'loadMaps': true})).
pipe(gulpPlugins.uglify({
'mangle': {'reserved': ['moment']}
})).
pipe(sourcemaps.write('./')).
pipe(gulp.dest('./'));
}
browser.on('update', () => {
bundler();
console.log('scripts rebuild');
});
return bundler();
}
gulp.task('scripts', () => {
bundle();
});
gulp.task('styles', () => {
gulp.src('css/styles.less').
pipe(gulpPlugins.plumber()).
pipe(sourcemaps.init()).
pipe(gulpPlugins.less({
'includePaths': ['.']
})).
pipe(gulpPlugins.autoprefixer()).
pipe(sourcemaps.write('./')).
pipe(gulp.dest('./'));
});
gulp.task('build', [
'scripts',
'styles'
]);
gulp.task('watch', () => {
bundle(true);
gulp.watch('css/**/*.less', ['styles']);
});
gulp.task('serve', ['watch'], () => {
browserSync({
'notify': false,
'port': 9000,
// Proxy: 'example.com',
'server': {
'baseDir': '.'
}
});
gulp.watch([
'**/*.html',
'**/*.php',
'styles.css',
'app.js'
]).on('change', browserSync.reload);
});
gulp.task('default', [
'build',
'serve'
]);

View File

@ -10,42 +10,10 @@
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/css/leaflet.css"> <link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/css/main.css"> <script src="/app.js"></script>
<link rel="stylesheet" href="/css/map.css">
<link rel="stylesheet" href="/css/console.css">
<script src="/js/moment.js"></script>
<script src="/js/navigo.js"></script>
<script src="/js/leaflet.js"></script>
<script src="/js/leaflet.ajax.min.js"></script>
<script src="/js/webgl-heatmap.js"></script>
<script src="/js/leaflet-webgl-heatmap.min.js"></script>
<script src="/js/config.js"></script>
<script src="/js/domlib.js"></script>
<script src="/js/store.js"></script>
<script src="/js/notify.js"></script>
<script src="/js/gui_list.js"></script>
<script src="/js/gui_map.js"></script>
<script src="/js/gui_node.js"></script>
<script src="/js/gui_stats.js"></script>
<script src="/js/gui.js"></script>
<script src="/js/socket.js"></script>
<script src="/js/app.js"></script>
</head> </head>
<body> <body>
<header>
<nav>
<ul>
<li class="logo"><img src="/img/logo.svg"></li>
<li class="item-1"><a href="#/list">List</a></li>
<li class="item-2"><a href="#/map">Map</a></li>
<li class="item-3"><a href="#/statistics">Statistics</a></li>
<li class="status offline"><span onclick="location.reload(true)"></span></li>
</ul>
</nav>
</header>
<div class="notifications"></div>
<main></main>
<noscript> <noscript>
<strong>JavaScript required</strong> <strong>JavaScript required</strong>
</noscript> </noscript>

View File

@ -1,11 +1,9 @@
/* exported config */
/* eslint no-magic-numbers: "off"*/ /* eslint no-magic-numbers: "off"*/
/* eslint sort-keys: "off"*/ /* eslint sort-keys: "off"*/
const config = { export default {
'title': 'FreifunkManager - Breminale', 'title': 'FreifunkManager - Breminale',
'backend': `ws${location.protocol == 'https:' ? 's' : ''}://${location.host}/websocket`, 'backend': `ws${location.protocol == 'https:' ? 's' : ''}://${location.host}/ws`,
'node': { 'node': {
// Minuten till is shown as offline // Minuten till is shown as offline
'offline': 5 'offline': 5
@ -20,22 +18,16 @@ const config = {
}, },
'maxZoom': 20, 'maxZoom': 20,
'tileLayer': 'https://tiles.bremen.freifunk.net/{z}/{x}/{y}.png', 'tileLayer': 'https://tiles.bremen.freifunk.net/{z}/{x}/{y}.png',
/* Heatmap settings
size: in meters (default: 30km)
opacity: in percent/100 (default: 1)
gradientTexture: url-to-texture-image (default: false)
alphaRange: change transparency in heatmap (default: 1)
autoresize: resize heatmap when map size changes (default: false)
*/
'heatmap': { 'heatmap': {
'wifi24': { 'wifi24': {
'size': 30, 'size': 10,
'units': 'm',
'opacity': 0.5, 'opacity': 0.5,
'alphaRange': 1 'alphaRange': 1
}, },
'wifi5': { 'wifi5': {
'size': 30, 'size': 10,
'units': 'm',
'opacity': 0.5, 'opacity': 0.5,
'alphaRange': 1 'alphaRange': 1
} }
@ -51,7 +43,7 @@ const config = {
} }
}, },
'geojson': { 'geojson': {
'url': 'https://events.ffhb.de/data/ground.geojson', 'url': 'https://raw.githubusercontent.com/FreifunkBremen/internal-maps/master/breminale.geojson',
'pointToLayer': function pointToLayer (feature, latlng) { 'pointToLayer': function pointToLayer (feature, latlng) {
'use strict'; 'use strict';

View File

@ -1,22 +1,54 @@
/* exported domlin */ export function setProps (el, props) {
if (props) {
if (props.class) {
let classList = props.class;
if (typeof props.class === 'string') {
classList = classList.split(' ');
}
el.classList.add(...classList);
delete props.class;
}
Object.keys(props).map((key) => {
if (key.indexOf('on') === 0 && typeof props[key] === 'function') {
// eslint-disable-next-line no-magic-numbers
return el.addEventListener(key.slice(2), props[key]);
}
return false;
});
Object.keys(props).map((key) => el.setAttribute(key, props[key]));
}
}
const domlib = {}; export function newEl (eltype, props, content) {
(function init () {
'use strict';
domlib.newAt = function newAt (at, eltype) {
const el = document.createElement(eltype); const el = document.createElement(eltype);
setProps(el, props);
if (content) {
el.innerHTML = content;
}
return el;
}
export function appendChild (el, child) {
if (child && !child.parentNode) {
el.appendChild(child);
}
}
export function removeChild (el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
// eslint-disable-next-line max-params
export function newAt (at, eltype, props, content) {
const el = document.createElement(eltype);
setProps(el, props);
if (content) {
el.innerHTML = content;
}
at.appendChild(el); at.appendChild(el);
return el; return el;
}; }
domlib.removeChildren = function removeChildren (el) {
if (el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
};
})();

101
webroot/js/element/menu.js Normal file
View File

@ -0,0 +1,101 @@
import * as V from 'superfine';
import * as domlib from '../domlib';
import * as socket from '../socket';
import * as store from '../store';
import View from '../view';
import {singelton as notify} from './notify';
import {render} from '../gui';
export const WINDOW_HEIGHT_MENU = 50;
export class MenuView extends View {
constructor () {
super();
this.el = document.createElement('header');
const menuContainer = domlib.newAt(this.el, 'nav');
this.menuList = domlib.newAt(menuContainer, 'ul');
const logo = domlib.newAt(this.menuList, 'li', {'class':'logo'});
domlib.newAt(logo, 'img', {'src':'/img/logo.svg'});
const aList = domlib.newAt(this.menuList, 'li', {'class':'item-1'});
domlib.newAt(aList, 'a', {'href':'#/list'}, 'List');
const aMap = domlib.newAt(this.menuList, 'li', {'class':'item-2'});
domlib.newAt(aMap, 'a', {'href':'#/map'}, 'Map');
const aStatistics= domlib.newAt(this.menuList, 'li', {'class':'item-3'});
domlib.newAt(aStatistics, 'a', {'href':'#/statistics'}, 'Statistics');
}
loginTyping(e) {
this._loginInput = e.target.value;
}
login() {
socket.sendjson({'subject': 'login', 'body': this._loginInput}, (msg) => {
if (msg.body) {
store.isLogin = true;
render();
}else {
notify.send({
'header': 'Anmeldung ist fehlgeschlagen',
'type': 'error'
}, 'Login');
}
});
this._loginInput = '';
}
logout() {
socket.sendjson({'subject': 'logout'}, (msg) => {
if (msg.body) {
store.isLogin = false;
render();
} else {
notify.send({
'header': 'Abmeldung ist fehlgeschlagen',
'type': 'error'
}, 'Logout');
}
});
}
render () {
const socketStatus = socket.getStatus();
let statusClass = 'status ',
vLogin = V.h('li', {
'class': 'login',
}, [
V.h('input', {
'type': 'password',
'value': this._loginInput,
'oninput': this.loginTyping.bind(this),
}),
V.h('a', {
'onclick': this.login.bind(this)
}, 'Login'
)
]);
if (store.isLogin) {
vLogin = V.h('li', {
'class': 'login',
'onclick': this.logout.bind(this)
}, 'Logout');
}
if (socketStatus !== 1) {
// eslint-disable-next-line no-magic-numbers
if (socketStatus === 0 || socketStatus === 2) {
statusClass += 'connecting';
} else {
statusClass += 'offline';
}
}
V.render(this.vMenu, this.vMenu = V.h('span',{},[V.h('li', {
'class': statusClass,
'onclick': () => location.reload(true)
}), vLogin]), this.menuList);
}
}

View File

@ -0,0 +1,91 @@
import * as V from 'superfine';
import View from '../view';
const DELAY_OF_NOTIFY = 15000,
MAX_MESSAGE_SHOW = 5;
class NotifyView extends View {
constructor () {
super();
this.el.classList.add('notifications');
if ('Notification' in window) {
window.Notification.requestPermission();
}
this.messages = [];
window.setInterval(this.removeLast.bind(this), DELAY_OF_NOTIFY);
}
removeLast () {
this.messages.splice(0, 1);
this.render();
}
renderMSG (msg) {
const {messages} = this,
content = [msg.content];
let {render} = this;
render = render.bind(this);
if (msg.header) {
content.unshift(V.h('div', {'class': 'header'}, msg.header));
}
return V.h(
'div', {
'class': `notify ${msg.type}`,
'onclick': () => {
const index = messages.indexOf(msg);
if (index !== -1) {
messages.splice(index, 1);
render();
}
}
}, V.h('div', {'class': 'content'}, content));
}
send (props, content) {
let msg = props;
if (typeof props === 'object') {
msg.content = content;
} else {
msg = {
'content': content,
'type': props
};
}
if ('Notification' in window &&
window.Notification.permission === 'granted') {
let body = msg.type,
title = content;
if (msg.header) {
title = msg.header;
body = msg.content;
}
// eslint-disable-next-line no-new
new window.Notification(title, {
'body': body,
'icon': '/favicon-32x32.png'
});
return;
}
if (this.messages.length > MAX_MESSAGE_SHOW) {
this.removeLast();
}
this.messages.push(msg);
this.render();
}
render () {
V.render(this.vel, this.vel = V.h('div', {'class': 'notifications'}, this.messages.map(this.renderMSG.bind(this))), this.el);
}
}
// eslint-disable-next-line one-var
const singelton = new NotifyView();
export {singelton, NotifyView};

View File

@ -1,101 +1,43 @@
/* exported gui,router */ import * as domlib from './domlib';
/* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode */ import {MenuView} from './element/menu';
import Navigo from '../node_modules/navigo/lib/navigo';
import View from './view';
import {singelton as notify} from './element/notify';
const gui = {}, const router = new Navigo(null, true, '#'),
router = new Navigo(null, true, '#'); elMain = domlib.newEl('main'),
elMenu = new MenuView();
export {router};
(function init () { let init = false,
'use strict'; currentView = new View();
const GUI_RENDER_DEBOUNCER_TIME = 100;
let currentView = {
'bind': function bind () {
console.warn('Do not run dummies');
},
// eslint-disable-next-line func-name-matching
'render': function renderDummy () {
console.warn('DO not run dummies');
}
};
function renderView () {
// eslint-disable-next-line prefer-destructuring
const status = document.getElementsByClassName('status')[0];
if (!status) {
console.log('unable to render, render later');
window.setTimeout(renderView, GUI_RENDER_DEBOUNCER_TIME);
export function render () {
if (!document.body) {
return; return;
} }
status.classList.remove('connecting', 'offline');
if (socket.readyState !== 1) {
let statusClass = 'offline';
// eslint-disable-next-line no-magic-numbers if (!init) {
if (socket.readyState === 0 || socket.readyState === 2) { elMenu.bind(document.body);
statusClass = 'connecting'; notify.bind(document.body);
}
status.classList.add(statusClass);
}
// eslint-disable-next-line prefer-destructuring document.body.appendChild(elMain);
notify.bind(document.getElementsByClassName('notifications')[0]);
init = true;
}
currentView.render(); currentView.render();
notify.render();
elMenu.render();
router.resolve(); router.resolve();
} }
function setView (toView) { export function setView (toView) {
currentView.unbind();
currentView = toView; currentView = toView;
const main = document.querySelector('main'); currentView.bind(elMain);
domlib.removeChildren(main);
currentView.bind(main);
currentView.render(); currentView.render();
} }
router.on({
'/list': function routerList () {
setView(guiList);
},
'/map': function routerMap () {
setView(guiMap);
},
'/n/:nodeID': {
'as': 'node',
// eslint-disable-next-line func-name-matching
'uses': function routerNode (params) {
guiNode.setNodeID(params.nodeID.toLowerCase());
setView(guiNode);
}
},
'/statistics': function routerStats () {
setView(guiStats);
}
});
router.on(() => {
router.navigate('/list');
});
gui.render = function render () {
let timeout = false;
function reset () {
timeout = null;
}
if (timeout) {
console('skip rendering, because to often');
window.clearTimeout(timeout);
} else {
renderView();
}
timeout = window.setTimeout(reset, GUI_RENDER_DEBOUNCER_TIME);
};
window.onload = gui.render;
})();

View File

@ -1,199 +0,0 @@
/* exported guiNode */
/* globals store, socket, domlib, config,notify */
const guiNode = {};
(function init () {
'use strict';
const view = guiNode;
let container = null,
el = null,
titleName = null,
titleID = null,
ago = null,
hostnameInput = null,
marker = null,
map = null,
geoJsonLayer = null,
btnGPS = null,
editLocationGPS = null,
storePosition = null,
currentNodeID = null,
editing = false;
function updatePosition (lat, lng) {
const node = store.getNode(currentNodeID),
newLat = lat || storePosition.latitude || false,
newlng = lng || storePosition.longitude || false;
if (!newLat || !newlng) {
return;
}
node.location = {
'latitude': newLat,
'longitude': newlng
};
socket.sendnode(node);
}
function update () {
geoJsonLayer.refresh();
titleID.innerHTML = currentNodeID;
const node = store.getNode(currentNodeID),
startdate = new Date();
if (!node) {
console.log(`node not found: ${currentNodeID}`);
return;
}
startdate.setMinutes(startdate.getMinutes() - config.node.offline);
if (new Date(node.lastseen) < startdate) {
ago.classList.add('offline');
ago.classList.remove('online');
} else {
ago.classList.remove('offline');
ago.classList.add('online');
}
ago.innerHTML = `${moment(node.lastseen).fromNow()} (${node.lastseen})`;
if (editLocationGPS || editing || !node.location || !node.location.latitude || !node.location.longitude) {
return;
}
titleName.innerHTML = node.hostname;
hostnameInput.value = node.hostname;
// eslint-disable-next-line one-var
const latlng = [node.location.latitude, node.location.longitude];
map.setView(latlng);
marker.setLatLng(latlng);
marker.setOpacity(1);
}
view.setNodeID = function setNodeID (nodeID) {
currentNodeID = nodeID;
};
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 node');
el = domlib.newAt(container, 'div');
const title = domlib.newAt(el, 'h1'),
lastseen = domlib.newAt(el, 'p'),
hostname = domlib.newAt(el, 'p'),
mapEl = domlib.newAt(el, 'div');
titleName = domlib.newAt(title, 'span');
title.appendChild(document.createTextNode(' - '));
titleID = domlib.newAt(title, 'i');
domlib.newAt(lastseen, 'span').innerHTML = 'Lastseen: ';
ago = domlib.newAt(lastseen, 'span');
domlib.newAt(hostname, 'span').innerHTML = 'Hostname: ';
hostnameInput = domlib.newAt(hostname, 'input');
hostnameInput.setAttribute('placeholder', 'Hostname');
hostnameInput.addEventListener('focusin', () => {
editing = true;
});
hostnameInput.addEventListener('focusout', () => {
editing = false;
const node = store.getNode(currentNodeID);
node.hostname = hostnameInput.value;
socket.sendnode(node);
});
mapEl.style.height = '300px';
map = L.map(mapEl).setView(config.map.view.bound, config.map.view.zoom);
L.tileLayer(config.map.tileLayer, {
'maxZoom': config.map.maxZoom
}).addTo(map);
geoJsonLayer = L.geoJson.ajax(config.map.geojson.url,
config.map.geojson);
geoJsonLayer.addTo(map);
marker = L.marker(config.map.view.bound, {'draggable': true,
'opacity': 0.5}).addTo(map);
marker.on('dragstart', () => {
editing = true;
});
marker.on('dragend', () => {
editing = false;
const pos = marker.getLatLng();
updatePosition(pos.lat, pos.lng);
});
btnGPS = domlib.newAt(el, 'span');
btnGPS.classList.add('btn');
btnGPS.innerHTML = 'Start follow position';
btnGPS.addEventListener('click', () => {
if (editLocationGPS) {
if (btnGPS.innerHTML === 'Stop following') {
updatePosition();
}
btnGPS.innerHTML = 'Start follow position';
navigator.geolocation.clearWatch(editLocationGPS);
editLocationGPS = false;
return;
}
btnGPS.innerHTML = 'Following position';
if (navigator.geolocation) {
editLocationGPS = navigator.geolocation.watchPosition((position) => {
btnGPS.innerHTML = 'Stop following';
storePosition = position.coords;
const latlng = [position.coords.latitude, position.coords.longitude];
marker.setLatLng(latlng);
map.setView(latlng);
}, (error) => {
switch (error.code) {
case error.TIMEOUT:
notify.send('error', 'Find Location timeout');
break;
default:
console.error('a navigator geolocation error: ', error);
}
},
{
'enableHighAccuracy': true,
'maximumAge': 30000,
'timeout': 27000
});
} else {
notify.send('error', 'Browser did not support Location');
}
});
update();
};
})();

View File

@ -1,33 +0,0 @@
/* exported guiSkel */
/* globals domlib */
const guiSkel = {};
(function init () {
'use strict';
const view = guiSkel;
let container = null,
el = null;
function update () {
console.warn('Do not run dummies');
}
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 skel');
el = domlib.newAt(container, 'div');
update();
};
})();

View File

@ -1,107 +0,0 @@
/* exported guiStats */
/* globals store, domlib */
const guiStats = {};
(function init () {
'use strict';
const view = guiStats;
let container = null,
el = null,
channelTabelle = null,
nodes = null,
clients = null,
clientsWifi = null,
clientsWifi24 = null,
clientsWifi5 = null;
function update () {
nodes.innerHTML = store.stats.Nodes;
clients.innerHTML = store.stats.Clients;
clientsWifi.innerHTML = store.stats.ClientsWifi;
clientsWifi24.innerHTML = store.stats.ClientsWifi24;
clientsWifi5.innerHTML = store.stats.ClientsWifi5;
domlib.removeChildren(channelTabelle);
let tr = domlib.newAt(channelTabelle, 'tr');
let title = domlib.newAt(tr, 'th');
title.innerHTML = '2.4 Ghz';
title.setAttribute('colspan', '2');
title = domlib.newAt(tr, 'th');
title.innerHTML = '5 Ghz';
title.setAttribute('colspan', '2');
const storeNodes = store.getNodes();
for (let ch = 1; ch <= 33; ch++) {
tr = domlib.newAt(channelTabelle, 'tr');
if (ch < 14) {
domlib.newAt(tr, 'td').innerHTML = ch;
domlib.newAt(tr, 'td').innerHTML = storeNodes.reduce((c, node) => node.wireless.channel24 === ch ? c + 1 : c, 0);
} else {
domlib.newAt(tr, 'td');
domlib.newAt(tr, 'td');
}
const ch5 = 32 + ch * 4;
domlib.newAt(tr, 'td').innerHTML = ch5;
domlib.newAt(tr, 'td').innerHTML = storeNodes.reduce((c, node) => node.wireless.channel5 === ch5 ? c + 1 : c, 0);
}
}
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 stats');
el = domlib.newAt(container, 'div');
domlib.newAt(el, 'h1').innerHTML = 'Statistics';
const table = domlib.newAt(el, 'table');
table.classList.add('stats');
let tr = domlib.newAt(table, 'tr'),
title = domlib.newAt(tr, 'th');
title.innerHTML = 'Nodes';
title.setAttribute('colspan', '2');
nodes = domlib.newAt(tr, 'td');
tr = domlib.newAt(table, 'tr');
title = domlib.newAt(tr, 'th');
title.innerHTML = 'Clients';
title.setAttribute('colspan', '2');
clients = domlib.newAt(tr, 'td');
tr = domlib.newAt(table, 'tr');
tr.classList.add('line');
domlib.newAt(tr, 'th').innerHTML = 'Wifi';
domlib.newAt(tr, 'th').innerHTML = '2.4 Ghz';
domlib.newAt(tr, 'th').innerHTML = '5 Ghz';
tr = domlib.newAt(table, 'tr');
clientsWifi = domlib.newAt(tr, 'td');
clientsWifi24 = domlib.newAt(tr, 'td');
clientsWifi5 = domlib.newAt(tr, 'td');
// Channels table
domlib.newAt(el, 'h1').innerHTML = 'Channels';
channelTabelle = domlib.newAt(el, 'table');
channelTabelle.classList.add('stats');
update();
};
})();

44
webroot/js/index.js Normal file
View File

@ -0,0 +1,44 @@
import '../node_modules/leaflet/dist/leaflet.js';
import '../node_modules/leaflet-ajax/dist/leaflet.ajax.min.js';
import '../node_modules/leaflet-webgl-heatmap/src/webgl-heatmap/webgl-heatmap.js';
import '../node_modules/leaflet-webgl-heatmap/src/leaflet-webgl-heatmap.js';
import * as gui from './gui';
import config from './config';
/**
* Self binding with router
*/
import {ListView} from './view/list';
import {MapView} from './view/map';
import {StatisticsView} from './view/statistics';
import {NodeView} from './view/node';
document.title = config.title;
window.onload = () => {
const listView = new ListView();
const mapView = new MapView();
const statisticsView = new StatisticsView();
const nodeView = new NodeView();
gui.router.on({
'/list': () => gui.setView(listView),
'/map': () => gui.setView(mapView),
'/statistics': () => gui.setView(statisticsView),
'/n/:nodeID': {
'as': 'node',
// eslint-disable-next-line func-name-matching
'uses': (params) => {
nodeView.setNodeID(params.nodeID.toLowerCase());
gui.setView(nodeView);
}
}
}).on(() => {
gui.router.navigate('/list');
});
gui.render();
}

View File

@ -1,6 +0,0 @@
/*
* MIT Copyright 2016 Ursudio <info@ursudio.com>
* http://www.ursudio.com/
* Please attribute Ursudio in any production associated with this JavaScript plugin.
*/
L.WebGLHeatMap=L.Renderer.extend({version:"0.2.2",options:{size:3e4,units:"m",opacity:1,gradientTexture:!1,alphaRange:1,padding:0},_initContainer:function(){var t=this._container=L.DomUtil.create("canvas","leaflet-zoom-animated"),i=this.options;t.id="webgl-leaflet-"+L.Util.stamp(this),t.style.opacity=i.opacity,t.style.position="absolute";try{this.gl=window.createWebGLHeatmap({canvas:t,gradientTexture:i.gradientTexture,alphaRange:[0,i.alphaRange]})}catch(t){console.error(t),this.gl={clear:function(){},update:function(){},multiply:function(){},addPoint:function(){},display:function(){},adjustSize:function(){}}}this._container=t},onAdd:function(){this.size=this.options.size,L.Renderer.prototype.onAdd.call(this),this.resize()},getEvents:function(){var t=L.Renderer.prototype.getEvents.call(this);return L.Util.extend(t,{resize:this.resize,move:L.Util.throttle(this._update,49,this)}),t},resize:function(){var t=this._container,i=this._map.getSize();t.width=i.x,t.height=i.y,this.gl.adjustSize(),this.draw()},reposition:function(){var t=this._map._getMapPanePos().multiplyBy(-1);L.DomUtil.setPosition(this._container,t)},_update:function(){L.Renderer.prototype._update.call(this),this.draw()},draw:function(){var t=this._map,i=this.gl,e=this.data,a=e.length,n=Math.floor,s=this["_scale"+this.options.units].bind(this),o=this._multiply;if(t){if(i.clear(),this.reposition(),a){for(var r=0;r<a;r++){var l=e[r],h=L.latLng(l),u=t.latLngToContainerPoint(h);i.addPoint(n(u.x),n(u.y),s(h),l[2])}i.update(),o&&(i.multiply(o),i.update())}i.display()}},_scalem:function(t){var i=this._map,e=this.size/40075017*360/Math.cos(Math.PI/180*t.lat),a=new L.LatLng(t.lat,t.lng-e),n=i.latLngToLayerPoint(t),s=i.latLngToLayerPoint(a);return Math.max(Math.round(n.x-s.x),1)},_scalepx:function(){return this.size},data:[],addDataPoint:function(t,i,e){this.data.push([t,i,e/100])},setData:function(t){this.data=t,this._multiply=null,this.draw()},clear:function(){this.setData([])},multiply:function(t){this._multiply=t,this.draw()}}),L.webGLHeatmap=function(t){return new L.WebGLHeatMap(t)};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25
webroot/js/lib.js Normal file
View File

@ -0,0 +1,25 @@
export function FromNowAgo(timeString) {
let time = new Date(timeString).getTime();
if(time <= 0) {
return 'NaN';
}
time = (new Date().getTime()) - time;
time /= 1000;
if (Math.abs(time) < 60) {
return Math.round(time) + ' s';
}
time /= 60;
if (Math.abs(time) < 60) {
return Math.round(time) + ' m';
}
time /= 60;
if (Math.abs(time) < 24) {
return Math.round(time) + ' h';
}
time /= 24;
return Math.round(time) + ' d';
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,67 +0,0 @@
/* exported notify */
const notify = {};
(function init () {
'use strict';
const DELAY_OF_NOTIFY = 15000,
MAX_MESSAGE_SHOW = 10,
messages = [];
let container = null;
if ('Notification' in window) {
window.Notification.requestPermission();
}
function removeLast () {
messages.splice(0, 1);
if (container && container.firstElementChild) {
container.removeChild(container.firstElementChild);
}
}
function renderMsg (msg) {
const msgBox = document.createElement('div');
msgBox.classList.add('notify', msg.type);
msgBox.innerHTML = msg.text;
container.appendChild(msgBox);
msgBox.addEventListener('click', () => {
container.removeChild(msgBox);
if (messages.indexOf(msg) !== -1) {
messages.splice(messages.indexOf(msg), 1);
}
});
}
window.setInterval(removeLast, DELAY_OF_NOTIFY);
notify.bind = function bind (el) {
container = el;
};
notify.send = function send (type, text) {
if ('Notification' in window &&
window.Notification.permission === 'granted') {
// eslint-disable-next-line no-new
new window.Notification(text, {
'body': type,
'icon': '/img/logo.jpg'
});
return;
}
if (messages.length > MAX_MESSAGE_SHOW) {
removeLast();
}
const msg = {
'text': text,
'type': type
};
messages.push(msg);
renderMsg(msg);
};
})();

View File

@ -1,89 +1,200 @@
/* exported socket */ import * as store from './store';
/* globals notify,gui,store,config*/ import config from './config';
let socket = {'readyState': 0}; import {singelton as notify} from './element/notify';
import {render} from './gui';
(function init () { const RECONNECT_AFTER = 5000,
'use strict'; RETRY_QUERY = 300,
query = [],
eventMSGID = {},
eventTo = {};
const RECONNECT_AFTER = 5000; let connectionID = localStorage.getItem('session'),
socket = null;
function onerror (err) { function newUUID () {
/* eslint-disable */
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
/* eslint-enable */
}
function correctMSG (obj) {
if (!obj.id) {
obj.id = newUUID();
}
}
function onerror (err) {
console.warn(err); console.warn(err);
// eslint-disable-next-line no-magic-numbers // eslint-disable-next-line no-magic-numbers
if (socket.readyState !== 3) { if (socket.readyState !== 3) {
notify.send('error', 'Es gibt Übertragungsprobleme!'); notify.send({
gui.render(); 'header': 'Verbindung',
'type': 'error'
}, 'Verbindung zum Server unterbrochen!');
} }
render();
socket.close();
}
function onopen () {
render();
}
export function sendjson (obj, callback) {
if (socket.readyState !== 1) {
query.push({
'callback': callback,
'obj': obj
});
return;
}
correctMSG(obj);
const socketMSG = JSON.stringify(obj);
socket.send(socketMSG);
if (typeof callback === 'function') {
eventMSGID[obj.id] = callback;
}
}
function onmessage (raw) {
const msg = JSON.parse(raw.data),
msgFunc = eventMSGID[msg.id],
eventFuncs = eventTo[msg.subject];
if (msg.subject === 'session_init') {
if (connectionID === null) {
connectionID = newUUID();
localStorage.setItem('session', connectionID);
}
msg.id = connectionID;
sendjson(msg);
render();
return;
} }
function onopen () { if (msgFunc) {
gui.render(); msgFunc(msg);
delete eventMSGID[msg.id];
render();
return;
} }
function onmessage (raw) { if (typeof eventFuncs === 'object' && eventFuncs.length > 0) {
const msg = JSON.parse(raw.data); // eslint-disable-next-line guard-for-in
for (const key in eventFuncs) {
switch (msg.type) { const func = eventFuncs[key];
case 'system': if (func) {
store.updateNode(msg.node, true); func(msg);
break;
case 'current':
store.updateNode(msg.node);
break;
case 'stats':
if (msg.body) {
store.stats = msg.body;
} }
break;
case 'cmd':
store.updateCMD(msg.cmd);
break;
default:
notify.send('warn', `unable to identify message: ${raw}`);
break;
} }
gui.render(); render();
return;
} }
function onclose () { notify.send('warning', `unable to identify message: ${msg.subject}`);
render();
}
function onclose () {
console.log('socket closed by server'); console.log('socket closed by server');
notify.send('warn', 'Es besteht ein Verbindungsproblem!'); notify.send({
gui.render(); 'header': 'Verbindung',
'type': 'warning'
}, 'Verbindung zum Server beendet!');
render();
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
window.setTimeout(connect, RECONNECT_AFTER); window.setTimeout(connect, RECONNECT_AFTER);
} }
function sendnode (node) { function connect () {
const notifyMsg = `Einstellungen für '${node.node_id}' gespeichert.`,
socketMsg = JSON.stringify({
'node': node,
'type': 'system'
});
socket.send(socketMsg);
notify.send('success', notifyMsg);
}
function sendcmd (cmd) {
const notifyMsg = `Befehl '${cmd.cmd}' wird überall ausgeführt.`,
socketMsg = JSON.stringify({
'cmd': cmd,
'type': 'cmd'
});
socket.send(socketMsg);
notify.send('success', notifyMsg);
}
function connect () {
socket = new window.WebSocket(config.backend); socket = new window.WebSocket(config.backend);
socket.onopen = onopen; socket.onopen = onopen;
socket.onerror = onerror; socket.onerror = onerror;
socket.onmessage = onmessage; socket.onmessage = onmessage;
socket.onclose = onclose; socket.onclose = onclose;
socket.sendnode = sendnode; sendjson({'subject': 'auth_status'});
socket.sendcmd = sendcmd; sendjson({'subject': 'connect'});
} }
connect(); window.setInterval(() => {
})(); const queryEntry = query.pop();
if (queryEntry) {
sendjson(queryEntry.obj, queryEntry.callback);
}
}, RETRY_QUERY);
export function getStatus () {
if (socket) {
return socket.readyState;
}
return 0;
}
export function setEvent (to, func) {
eventTo[to] = [func];
}
export function addEvent (to, func) {
if (typeof eventTo[to] !== 'object') {
eventTo[to] = [];
}
eventTo[to].push(func);
}
export function delEvent (to, func) {
if (typeof eventTo[to] === 'object' && eventTo[to].length > 1) {
eventTo[to].pop(func);
} else {
eventTo[to] = [];
}
}
export function sendnode(node, callback) {
sendjson({'subject':'node-system','body': node}, (msg) => {
if(msg.body){
notify.send({
'header': 'Speichern',
'type': 'success',
}, `Einstellungen für '${node.node_id}' gespeichert.`);
}else{
notify.send({
'header': 'Speichern',
'type': 'error',
}, `Einstellungen für '${node.node_id}' wurden nicht gespeichert.`);
}
if (typeof callback === 'function') {
callback(msg);
}
});
}
setEvent('auth_status', (msg) => {
if (msg.body) {
store.isLogin = true;
notify.send({
'header': 'Login',
'type': 'success'
},'Willkommen zurück!');
} else {
store.isLogin = false;
}
render();
});
setEvent('node-system', (msg) => {
store.updateNode(msg.body, true);
});
setEvent('node-current', (msg) => {
store.updateNode(msg.body);
});
connect();

View File

@ -1,27 +1,7 @@
/* exported store */ const current = {},
list = {};
export function getNode (nodeid) {
const store = {
'stats': {
'Clients': 0,
'ClientsWifi': 0,
'ClientsWifi24': 0,
'ClientsWifi5': 0,
'Firmwares': {},
'Gateways': 0,
'Models': {},
'Nodes': 0
}
};
(function init () {
'use strict';
const current = {},
list = {},
cmds = {};
function getNode (nodeid) {
let node = {}; let node = {};
if (list[nodeid]) { if (list[nodeid]) {
@ -42,28 +22,16 @@ const store = {
} }
return node; return node;
} };
store.updateNode = function updateNode (node, system) { export function updateNode (node, system) {
if (system) { if (system) {
list[node.node_id] = node; list[node.node_id] = node;
} else { } else {
current[node.node_id] = node; current[node.node_id] = node;
} }
}; };
export function getNodes () {
store.getNode = getNode;
store.getNodes = function getNodes () {
return Object.keys(list).map(getNode); return Object.keys(list).map(getNode);
}; };
store.updateCMD = function updateCMD (cmd) {
cmds[cmd.id] = cmd;
};
store.getCMDs = function getCMDs () {
return cmds;
};
})();

22
webroot/js/view.js Normal file
View File

@ -0,0 +1,22 @@
export default class View {
constructor () {
this.el = document.createElement('div');
}
unbind () {
if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
} else {
console.warn('unbind view not possible');
}
}
bind (el) {
el.appendChild(this.el);
}
// eslint-disable-next-line class-methods-use-this
render () {
//console.log('abstract view');
}
}

View File

@ -1,32 +1,130 @@
/* exported guiList */ import * as domlib from '../domlib';
/* global config,domlib,store,router,socket */ import * as gui from '../gui';
/* eslint max-lines: [off] */ import * as socket from '../socket';
import * as store from '../store';
import config from '../config';
import View from '../view';
import {FromNowAgo} from '../lib';
const guiList = {}; export class ListView extends View {
(function init () { constructor () {
'use strict'; super();
const table = domlib.newAt(this.el, 'table'),
thead = domlib.newAt(table, 'thead');
const view = guiList; this.tbody = domlib.newAt(table, 'tbody');
let container = null, // eslint-disable-next-line one-var
el = null, const tr = domlib.newAt(thead, 'tr'),
tbody = null, cell1 = domlib.newAt(tr, 'th'),
sortReverse = false, cell2 = domlib.newAt(tr, 'th'),
sortIndex = null, cell3 = domlib.newAt(tr, 'th'),
hostnameFilter = null, cell4 = domlib.newAt(tr, 'th'),
nodeidFilter = null, cell5 = domlib.newAt(tr, 'th'),
editing = false; cell6 = domlib.newAt(tr, 'th'),
cell7 = domlib.newAt(tr, 'th'),
cell8 = domlib.newAt(tr, 'th'),
cell9 = domlib.newAt(tr, 'th'),
cell10 = domlib.newAt(tr, 'th'),
cell11 = domlib.newAt(tr, 'th');
cell1.innerHTML = 'Lastseen';
cell1.addEventListener('click', () => {
this.sortTable(cell1);
});
cell2.classList.add('sortable');
this.nodeidFilter = domlib.newAt(cell2, 'input');
this.nodeidFilter.setAttribute('placeholder', 'NodeID');
this.nodeidFilter.setAttribute('size', '9');
this.nodeidFilter.addEventListener('keyup', this.render);
cell2.addEventListener('dblclick', () => {
this.sortTable(cell2);
});
this.nodeidFilter.addEventListener('focusin', () => {
this.editing = true;
});
this.nodeidFilter.addEventListener('focusout', () => {
this.editing = false;
this.render();
});
cell3.classList.add('sortable');
cell3.classList.add('hostname');
this.hostnameFilter = domlib.newAt(cell3, 'input');
this.hostnameFilter.setAttribute('placeholder', 'Hostname');
this.hostnameFilter.addEventListener('keyup', this.render);
cell3.addEventListener('dblclick', () => {
this.sortTable(cell3);
});
this.hostnameFilter.addEventListener('focusin', () => {
this.editing = true;
});
this.hostnameFilter.addEventListener('focusout', () => {
this.editing = false;
this.render();
});
cell4.innerHTML = 'Freq';
cell5.innerHTML = 'CurChannel';
cell5.classList.add('sortable');
cell5.addEventListener('click', () => {
this.sortTable(cell5);
});
cell6.innerHTML = 'Channel';
cell6.classList.add('sortable');
cell6.addEventListener('click', () => {
this.sortTable(cell6);
});
cell7.innerHTML = 'CurPower';
cell7.classList.add('sortable');
cell7.addEventListener('click', () => {
this.sortTable(cell7);
});
cell8.innerHTML = 'Power';
cell8.classList.add('sortable');
cell8.addEventListener('click', () => {
this.sortTable(cell8);
});
cell9.innerHTML = 'Clients';
cell9.classList.add('sortable');
cell9.addEventListener('click', () => {
this.sortTable(cell9);
});
cell10.innerHTML = 'ChanUtil';
cell10.classList.add('sortable');
cell10.addEventListener('click', () => {
this.sortTable(cell10);
});
cell11.innerHTML = 'Option';
table.classList.add('nodes');
}
// eslint-disable-next-line id-length // eslint-disable-next-line id-length
function sort (a, b) { sort (sortIndex, sortReverse) {
function sortNumber (aNum, bNum) { function sortNumber (aNum, bNum) {
return aNum - bNum; return aNum - bNum;
} }
return (a, b) => {
if (!sortIndex) { if (!sortIndex) {
return a.node_id.localeCompare(b.node_id); return a.node_id.localeCompare(b.node_id);
} }
switch (sortIndex.innerHTML) { if (sortIndex.classList.contains("hostname")) {
return a.hostname.localeCompare(b.hostname);
}
switch (sortIndex.innerText) {
case 'Lastseen': case 'Lastseen':
return a.lastseen - b.lastseen; return a.lastseen - b.lastseen;
case 'CurPower': case 'CurPower':
@ -43,14 +141,12 @@ const guiList = {};
return a.statistics.clients.wifi24 - b.statistics.clients.wifi24; return a.statistics.clients.wifi24 - b.statistics.clients.wifi24;
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
case 'ChanUtil': case 'ChanUtil':
if(a.statistics.wireless === null) return 1;
if(b.statistics.wireless === null) return -1;
// eslint-disable-next-line id-length // eslint-disable-next-line id-length
let aMax = a.statistics.wireless.map((d) => let aMax = a.statistics.wireless.map((d) => d.ChanUtil).sort(sortNumber),
d.ChanUtil
).sort(sortNumber),
// eslint-disable-next-line id-length // eslint-disable-next-line id-length
bMax = b.statistics.wireless.map((d) => bMax = b.statistics.wireless.map((d) => d.ChanUtil).sort(sortNumber);
d.ChanUtil
).sort(sortNumber);
if (!sortReverse) { if (!sortReverse) {
aMax = aMax.reverse(); aMax = aMax.reverse();
@ -58,14 +154,13 @@ const guiList = {};
} }
return bMax[0] - aMax[0]; return bMax[0] - aMax[0];
case 'Hostname':
return a.hostname.localeCompare(b.hostname);
default: default:
return a.node_id.localeCompare(b.node_id); return a.node_id.localeCompare(b.node_id);
} }
} }
}
function renderRow (node) { renderRow (node) {
const startdate = new Date(), const startdate = new Date(),
tr = document.createElement('tr'), tr = document.createElement('tr'),
lastseen = domlib.newAt(tr, 'td'), lastseen = domlib.newAt(tr, 'td'),
@ -95,7 +190,7 @@ const guiList = {};
} }
lastseen.innerHTML = moment(node.lastseen).fromNow(true); lastseen.innerHTML = FromNowAgo(node.lastseen);
nodeID.innerHTML = node.node_id; nodeID.innerHTML = node.node_id;
@ -103,17 +198,23 @@ const guiList = {};
hostnameInput.readOnly = true; hostnameInput.readOnly = true;
hostnameInput.setAttribute('placeholder', 'Hostname'); hostnameInput.setAttribute('placeholder', 'Hostname');
hostnameInput.addEventListener('dblclick', () => { hostnameInput.addEventListener('dblclick', () => {
editing = true; this.editing = true;
hostnameInput.readOnly = false; hostnameInput.readOnly = false;
}); });
hostnameInput.addEventListener('focusout', () => { hostnameInput.addEventListener('focusout', () => {
if (hostnameInput.readOnly) { if (hostnameInput.readOnly) {
return; return;
} }
editing = false; this.editing = false;
hostnameInput.readOnly = true; hostnameInput.readOnly = true;
const old = node.hostname;
node.hostname = hostnameInput.value; node.hostname = hostnameInput.value;
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.hostname = old;
hostnameInput.value = old;
}
});
}); });
domlib.newAt(freq, 'span').innerHTML = '2.4 Ghz'; domlib.newAt(freq, 'span').innerHTML = '2.4 Ghz';
@ -134,17 +235,23 @@ const guiList = {};
channel24Input.readOnly = true; channel24Input.readOnly = true;
channel24Input.setAttribute('placeholder', '-'); channel24Input.setAttribute('placeholder', '-');
channel24Input.addEventListener('dblclick', () => { channel24Input.addEventListener('dblclick', () => {
editing = true; this.editing = true;
channel24Input.readOnly = false; channel24Input.readOnly = false;
}); });
channel24Input.addEventListener('focusout', () => { channel24Input.addEventListener('focusout', () => {
if (channel24Input.readOnly) { if (channel24Input.readOnly) {
return; return;
} }
editing = false; this.editing = false;
channel24Input.readOnly = true; channel24Input.readOnly = true;
const old = node.wireless.channel24;
node.wireless.channel24 = parseInt(channel24Input.value, 10); node.wireless.channel24 = parseInt(channel24Input.value, 10);
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.wireless.channel24 = old;
channel24Input.value = old;
}
});
}); });
channel5Input.value = node.wireless.channel5 || ''; channel5Input.value = node.wireless.channel5 || '';
@ -155,17 +262,23 @@ const guiList = {};
channel5Input.readOnly = true; channel5Input.readOnly = true;
channel5Input.setAttribute('placeholder', '-'); channel5Input.setAttribute('placeholder', '-');
channel5Input.addEventListener('dblclick', () => { channel5Input.addEventListener('dblclick', () => {
editing = true; this.editing = true;
channel5Input.readOnly = false; channel5Input.readOnly = false;
}); });
channel5Input.addEventListener('focusout', () => { channel5Input.addEventListener('focusout', () => {
if (channel5Input.readOnly) { if (channel5Input.readOnly) {
return; return;
} }
editing = false; this.editing = false;
channel5Input.readOnly = true; channel5Input.readOnly = true;
const old = node.wireless.channel5;
node.wireless.channel5 = parseInt(channel5Input.value, 10); node.wireless.channel5 = parseInt(channel5Input.value, 10);
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.wireless.channel5 = old;
channel5Input.value = old;
}
});
}); });
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
@ -182,17 +295,23 @@ const guiList = {};
power24Input.readOnly = true; power24Input.readOnly = true;
power24Input.setAttribute('placeholder', '-'); power24Input.setAttribute('placeholder', '-');
power24Input.addEventListener('dblclick', () => { power24Input.addEventListener('dblclick', () => {
editing = true; this.editing = true;
power24Input.readOnly = false; power24Input.readOnly = false;
}); });
power24Input.addEventListener('focusout', () => { power24Input.addEventListener('focusout', () => {
if (power24Input.readOnly) { if (power24Input.readOnly) {
return; return;
} }
editing = false; this.editing = false;
power24Input.readOnly = true; power24Input.readOnly = true;
const old = node.wireless.txpower24;
node.wireless.txpower24 = parseInt(power24Input.value, 10); node.wireless.txpower24 = parseInt(power24Input.value, 10);
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.wireless.txpower24 = old;
power24Input.value = old;
}
});
}); });
power5Input.value = node.wireless.txpower5 || ''; power5Input.value = node.wireless.txpower5 || '';
@ -202,17 +321,23 @@ const guiList = {};
power5Input.readOnly = true; power5Input.readOnly = true;
power5Input.setAttribute('placeholder', '-'); power5Input.setAttribute('placeholder', '-');
power5Input.addEventListener('dblclick', () => { power5Input.addEventListener('dblclick', () => {
editing = true; this.editing = true;
power5Input.readOnly = false; power5Input.readOnly = false;
}); });
power5Input.addEventListener('focusout', () => { power5Input.addEventListener('focusout', () => {
if (power5Input.readOnly) { if (power5Input.readOnly) {
return; return;
} }
editing = false; this.editing = false;
power5Input.readOnly = true; power5Input.readOnly = true;
const old = node.wireless.txpower5;
node.wireless.txpower5 = parseInt(power5Input.value, 10); node.wireless.txpower5 = parseInt(power5Input.value, 10);
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.wireless.txpower5 = old;
power5Input.value = old;
}
});
}); });
domlib.newAt(client, 'span').innerHTML = node.statistics.clients.wifi24; domlib.newAt(client, 'span').innerHTML = node.statistics.clients.wifi24;
@ -233,173 +358,58 @@ const guiList = {};
edit.classList.add('btn'); edit.classList.add('btn');
edit.innerHTML = 'Edit'; edit.innerHTML = 'Edit';
edit.addEventListener('click', () => { edit.addEventListener('click', () => {
router.navigate(router.generate('node', {'nodeID': node.node_id})); gui.router.navigate(gui.router.generate('node', {'nodeID': node.node_id}));
}); });
return tr; return tr;
} }
function update () {
if (editing) {
sortTable (head) {
if (this.sortIndex) {
this.sortIndex.classList.remove('sort-up', 'sort-down');
}
this.sortReverse = head === this.sortIndex
? !this.sortReverse
: false;
this.sortIndex = head;
this.sortIndex.classList.add(this.sortReverse
? 'sort-up'
: 'sort-down');
this.render();
}
render () {
if (this.editing && this.tbody) {
return; return;
} }
domlib.removeChildren(tbody); while(this.tbody.hasChildNodes()) {
this.tbody.removeChild(this.tbody.firstElementChild);
}
let nodes = store.getNodes(); let nodes = store.getNodes();
if (hostnameFilter && hostnameFilter.value !== '') { if (this.hostnameFilter && this.hostnameFilter.value !== '') {
// eslint-disable-next-line id-length // eslint-disable-next-line id-length
nodes = nodes.filter((d) => d.hostname.toLowerCase().indexOf(hostnameFilter.value) > -1); nodes = nodes.filter((d) => d.hostname.toLowerCase().indexOf(this.hostnameFilter.value.toLowerCase()) > -1);
} }
if (nodeidFilter && nodeidFilter.value !== '') { if (this.nodeidFilter && this.nodeidFilter.value !== '') {
// eslint-disable-next-line id-length // eslint-disable-next-line id-length
nodes = nodes.filter((d) => d.node_id.indexOf(nodeidFilter.value) > -1); nodes = nodes.filter((d) => d.node_id.indexOf(this.nodeidFilter.value.toLowerCase()) > -1);
} }
nodes = nodes.sort(sort); nodes = nodes.sort(this.sort(this.sortIndex, this.sortReverse));
if (sortReverse) { if (this.sortReverse) {
nodes = nodes.reverse(); nodes = nodes.reverse();
} }
for (let i = 0; i < nodes.length; i += 1) { for (let i = 0; i < nodes.length; i += 1) {
const row = renderRow(nodes[i]); const row = this.renderRow(nodes[i]);
tbody.appendChild(row); this.tbody.appendChild(row);
} }
} }
}
function sortTable (head) {
if (sortIndex) {
sortIndex.classList.remove('sort-up', 'sort-down');
}
sortReverse = head === sortIndex
? !sortReverse
: false;
sortIndex = head;
sortIndex.classList.add(sortReverse
? 'sort-up'
: 'sort-down');
update();
}
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 list');
el = domlib.newAt(container, 'div');
const table = domlib.newAt(el, 'table'),
thead = domlib.newAt(table, 'thead');
tbody = domlib.newAt(table, 'tbody');
// eslint-disable-next-line one-var
const tr = domlib.newAt(thead, 'tr'),
cell1 = domlib.newAt(tr, 'th'),
cell2 = domlib.newAt(tr, 'th'),
cell3 = domlib.newAt(tr, 'th'),
cell4 = domlib.newAt(tr, 'th'),
cell5 = domlib.newAt(tr, 'th'),
cell6 = domlib.newAt(tr, 'th'),
cell7 = domlib.newAt(tr, 'th'),
cell8 = domlib.newAt(tr, 'th'),
cell9 = domlib.newAt(tr, 'th'),
cell10 = domlib.newAt(tr, 'th'),
cell11 = domlib.newAt(tr, 'th');
cell1.innerHTML = 'Lastseen';
cell1.addEventListener('click', () => {
sortTable(cell1);
});
cell2.classList.add('sortable');
nodeidFilter = domlib.newAt(cell2, 'input');
nodeidFilter.setAttribute('placeholder', 'NodeID');
nodeidFilter.setAttribute('size', '9');
nodeidFilter.addEventListener('keyup', update);
cell2.addEventListener('dblclick', () => {
sortTable(cell2);
});
nodeidFilter.addEventListener('focusin', () => {
editing = true;
});
nodeidFilter.addEventListener('focusout', () => {
editing = false;
update();
});
cell3.classList.add('sortable');
hostnameFilter = domlib.newAt(cell3, 'input');
hostnameFilter.setAttribute('placeholder', 'Hostname');
hostnameFilter.addEventListener('keyup', update);
cell3.addEventListener('dblclick', () => {
sortTable(cell3);
});
hostnameFilter.addEventListener('focusin', () => {
editing = true;
});
hostnameFilter.addEventListener('focusout', () => {
editing = false;
update();
});
cell4.innerHTML = 'Freq';
cell5.innerHTML = 'CurChannel';
cell5.classList.add('sortable');
cell5.addEventListener('click', () => {
sortTable(cell4);
});
cell6.innerHTML = 'Channel';
cell6.classList.add('sortable');
cell6.addEventListener('click', () => {
sortTable(cell5);
});
cell7.innerHTML = 'CurPower';
cell7.classList.add('sortable');
cell7.addEventListener('click', () => {
sortTable(cell6);
});
cell8.innerHTML = 'Power';
cell8.classList.add('sortable');
cell8.addEventListener('click', () => {
sortTable(cell7);
});
cell9.innerHTML = 'Clients';
cell9.classList.add('sortable');
cell9.addEventListener('click', () => {
sortTable(cell8);
});
cell10.innerHTML = 'ChanUtil';
cell10.classList.add('sortable');
cell10.addEventListener('click', () => {
sortTable(cell9);
});
cell11.innerHTML = 'Option';
table.classList.add('nodes');
update();
};
})();

View File

@ -1,24 +1,52 @@
/* exported guiMap */
/* global config,store,domlib,socket */
const guiMap = {}; import * as domlib from '../domlib';
import * as gui from '../gui';
import * as socket from '../socket';
import * as store from '../store';
import config from '../config';
import View from '../view';
import {WINDOW_HEIGHT_MENU} from '../element/menu';
//import '../../node_modules/leaflet/dist/leaflet.js';
//import '../../node_modules/leaflet-webgl-heatmap/dist/leaflet-webgl-heatmap.min.js';
//import '../../node_modules/leaflet-ajax/dist/leaflet.ajax.min.js';
(function init () {
'use strict';
const view = guiMap, export class MapView extends View {
WINDOW_HEIGHT_MENU = 50;
let container = null, constructor () {
el = null, super();
geoJsonLayer = null,
nodeLayer = null,
clientLayer24 = null,
clientLayer5 = null;
// , draggingNodeID=null;
function addNode (node) { this.el.style.height = `${window.innerHeight - WINDOW_HEIGHT_MENU}px`;
this.map = L.map(this.el).setView(config.map.view.bound, config.map.view.zoom);
const layerControl = L.control.layers().addTo(this.map);
L.tileLayer(config.map.tileLayer, {
'maxZoom': config.map.maxZoom
}).addTo(this.map);
this.geoJsonLayer = L.geoJson.ajax(config.map.geojson.url, config.map.geojson);
this.nodeLayer = L.layerGroup();
/* eslint-disable new-cap */
this.clientLayer24 = L.webGLHeatmap(config.map.heatmap.wifi24);
this.clientLayer5 = L.webGLHeatmap(config.map.heatmap.wifi5);
/* eslint-enable new-cap */
layerControl.addOverlay(this.geoJsonLayer, 'geojson');
layerControl.addOverlay(this.nodeLayer, 'Nodes');
layerControl.addOverlay(this.clientLayer24, 'Clients 2.4 Ghz');
layerControl.addOverlay(this.clientLayer5, 'Clients 5 Ghz');
this.nodeLayer.addTo(this.map);
window.addEventListener('resize', () => {
this.el.style.height = `${window.innerHeight - WINDOW_HEIGHT_MENU}px`;
this.map.invalidateSize();
});
}
addNode (node) {
/* eslint-disable-line https://github.com/Leaflet/Leaflet/issues/4484 /* eslint-disable-line https://github.com/Leaflet/Leaflet/issues/4484
if(node.node_id === draggingNodeID){ if(node.node_id === draggingNodeID){
return return
@ -88,29 +116,32 @@ const guiMap = {};
*/ */
nodemarker.on('dragend', () => { nodemarker.on('dragend', () => {
// DraggingNodeID = undefined; // DraggingNodeID = undefined;
const pos = nodemarker.getLatLng(); const pos = nodemarker.getLatLng(),
old = node.location;
node.location = { node.location = {
'latitude': pos.lat, 'latitude': pos.lat,
'longitude': pos.lng 'longitude': pos.lng
}; };
socket.sendnode(node); socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.location = old;
}
}); });
nodeLayer.addLayer(nodemarker); });
this.nodeLayer.addLayer(nodemarker);
} }
function update () { render () {
geoJsonLayer.refresh(); this.geoJsonLayer.refresh();
nodeLayer.clearLayers(); this.nodeLayer.clearLayers();
const nodes = store.getNodes(); const nodes = store.getNodes();
for (let i = 0; i < nodes.length; i += 1) { for (let i = 0; i < nodes.length; i += 1) {
addNode(nodes[i]); this.addNode(nodes[i]);
} }
this.clientLayer24.setData(nodes.map((node) => {
clientLayer24.setData(nodes.map((node) => {
if (!node.location || !node.location.latitude || !node.location.longitude) { if (!node.location || !node.location.latitude || !node.location.longitude) {
return null; return null;
} }
@ -118,59 +149,13 @@ const guiMap = {};
return [node.location.latitude, node.location.longitude, node.statistics.clients.wifi24 || 0]; return [node.location.latitude, node.location.longitude, node.statistics.clients.wifi24 || 0];
})); }));
clientLayer5.setData(nodes.map((node) => { this.clientLayer5.setData(nodes.map((node) => {
if (!node.location || !node.location.latitude || !node.location.longitude) { if (!node.location || !node.location.latitude || !node.location.longitude) {
return null; return null;
} }
return [node.location.latitude, node.location.longitude, node.statistics.clients.wifi5 || 0]; return [node.location.latitude, node.location.longitude, node.statistics.clients.wifi5 || 0];
})); }));
this.map.invalidateSize();
} }
}
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 map');
el = domlib.newAt(container, 'div');
el.style.height = `${window.innerHeight - WINDOW_HEIGHT_MENU}px`;
const map = L.map(el).setView(config.map.view.bound, config.map.view.zoom),
layerControl = L.control.layers().addTo(map);
L.tileLayer(config.map.tileLayer, {
'maxZoom': config.map.maxZoom
}).addTo(map);
geoJsonLayer = L.geoJson.ajax(config.map.geojson.url, config.map.geojson);
nodeLayer = L.layerGroup();
/* eslint-disable new-cap */
clientLayer24 = new L.webGLHeatmap(config.map.heatmap.wifi24);
clientLayer5 = new L.webGLHeatmap(config.map.heatmap.wifi5);
/* eslint-enable new-cap */
layerControl.addOverlay(geoJsonLayer, 'geojson');
layerControl.addOverlay(nodeLayer, 'Nodes');
layerControl.addOverlay(clientLayer24, 'Clients 2.4 Ghz');
layerControl.addOverlay(clientLayer5, 'Clients 5 Ghz');
nodeLayer.addTo(map);
window.addEventListener('resize', () => {
el.style.height = `${window.innerHeight - WINDOW_HEIGHT_MENU}px`;
map.invalidateSize();
});
update();
};
})();

203
webroot/js/view/node.js Normal file
View File

@ -0,0 +1,203 @@
import * as domlib from '../domlib';
import * as gui from '../gui';
import * as socket from '../socket';
import * as store from '../store';
import config from '../config';
import View from '../view';
import {singelton as notify} from '../element/notify';
import {FromNowAgo} from '../lib';
//import '../../node_modules/leaflet/dist/leaflet.js';
//import '../../node_modules/leaflet-ajax/dist/leaflet.ajax.min.js';
//import '../../node_modules/moment/min/moment.min.js';
export class NodeView extends View {
constructor () {
super();
const title = domlib.newAt(this.el, 'h1'),
lastseen = domlib.newAt(this.el, 'p'),
hostname = domlib.newAt(this.el, 'p'),
owner = domlib.newAt(this.el, 'p'),
mapEl = domlib.newAt(this.el, 'div');
this.titleName = domlib.newAt(title, 'span');
title.appendChild(document.createTextNode(' - '));
this.titleID = domlib.newAt(title, 'i');
domlib.newAt(lastseen, 'span').innerHTML = 'Lastseen: ';
this.ago = domlib.newAt(lastseen, 'span');
domlib.newAt(hostname, 'span').innerHTML = 'Hostname: ';
this.hostnameInput = domlib.newAt(hostname, 'input');
this.hostnameInput.setAttribute('placeholder', 'Hostname');
this.hostnameInput.addEventListener('focusin', () => {
this.editing = true;
});
this.hostnameInput.addEventListener('focusout', () => {
this.editing = false;
const node = store.getNode(this.currentNodeID),
old = node.hostname;
node.hostname = this.hostnameInput.value;
socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.hostname = old;
}
});
});
domlib.newAt(owner, 'span').innerHTML = 'Owner: ';
this.ownerInput = domlib.newAt(owner, 'input');
this.ownerInput.setAttribute('placeholder', 'Owner');
this.ownerInput.addEventListener('focusin', () => {
this.editing = true;
});
this.ownerInput.addEventListener('focusout', () => {
this.editing = false;
const node = store.getNode(this.currentNodeID),
old = node.owner;
node.owner = this.ownerInput.value;
socket.sendnode(node, (msg)=>{
if (!msg.body) {
node.owner = old;
}
});
});
mapEl.style.height = '300px';
this.map = L.map(mapEl).setView(config.map.view.bound, config.map.view.zoom);
L.tileLayer(config.map.tileLayer, {
'maxZoom': config.map.maxZoom
}).addTo(this.map);
this.geoJsonLayer = L.geoJson.ajax(config.map.geojson.url,
config.map.geojson);
this.geoJsonLayer.addTo(this.map);
this.marker = L.marker(config.map.view.bound, {'draggable': true,
'opacity': 0.5}).addTo(this.map);
this.marker.on('dragstart', () => {
this.editing = true;
});
this.marker.on('dragend', () => {
this.editing = false;
const pos = this.marker.getLatLng();
this.updatePosition(pos.lat, pos.lng);
});
this.btnGPS = domlib.newAt(this.el, 'span');
this.btnGPS.classList.add('btn');
this.btnGPS.innerHTML = 'Start follow position';
this.btnGPS.addEventListener('click', () => {
if (this.editLocationGPS) {
if (this.btnGPS.innerHTML === 'Stop following') {
updatePosition();
}
this.btnGPS.innerHTML = 'Start follow position';
navigator.geolocation.clearWatch(this.editLocationGPS);
this.editLocationGPS = false;
return;
}
this.btnGPS.innerHTML = 'Following position';
if (navigator.geolocation) {
this.editLocationGPS = navigator.geolocation.watchPosition((position) => {
this.btnGPS.innerHTML = 'Stop following';
this.this.storePosition = position.coords;
const latlng = [position.coords.latitude, position.coords.longitude];
this.marker.setLatLng(latlng);
this.map.setView(latlng);
}, (error) => {
switch (error.code) {
case error.TIMEOUT:
notify.send('error', 'Find Location timeout');
break;
default:
console.error('a navigator geolocation error: ', error);
}
},
{
'enableHighAccuracy': true,
'maximumAge': 30000,
'timeout': 27000
});
} else {
notify.send('error', 'Browser did not support Location');
}
});
}
updatePosition (lat, lng) {
const node = store.getNode(this.currentNodeID),
newLat = lat || this.storePosition.latitude || false,
newlng = lng || this.storePosition.longitude || false;
if (!newLat || !newlng) {
return;
}
node.location = {
'latitude': newLat,
'longitude': newlng
};
socket.sendnode(node);
}
render () {
this.geoJsonLayer.refresh();
this.titleID.innerHTML = this.currentNodeID;
const node = store.getNode(this.currentNodeID),
startdate = new Date();
if (!node) {
console.log(`node not found: ${this.currentNodeID}`);
return;
}
startdate.setMinutes(startdate.getMinutes() - config.node.offline);
if (new Date(node.lastseen) < startdate) {
this.ago.classList.add('offline');
this.ago.classList.remove('online');
} else {
this.ago.classList.remove('offline');
this.ago.classList.add('online');
}
this.ago.innerHTML = `${FromNowAgo(node.lastseen)} (${node.lastseen})`;
if (this.editLocationGPS || this.editing || !node.location || !node.location.latitude || !node.location.longitude) {
return;
}
this.titleName.innerHTML = node.hostname;
this.hostnameInput.value = node.hostname;
this.ownerInput.value = node.owner;
// eslint-disable-next-line one-var
const latlng = [node.location.latitude, node.location.longitude];
this.map.setView(latlng);
this.marker.setLatLng(latlng);
this.marker.setOpacity(1);
this.map.invalidateSize();
}
setNodeID (nodeID) {
this.currentNodeID = nodeID;
}
}

View File

@ -0,0 +1,84 @@
import * as domlib from '../domlib';
import * as gui from '../gui';
import * as socket from '../socket';
import * as store from '../store';
import View from '../view';
export class StatisticsView extends View {
constructor () {
super();
domlib.newAt(this.el, 'h1').innerHTML = 'Statistics';
const table = domlib.newAt(this.el, 'table');
table.classList.add('stats');
let tr = domlib.newAt(table, 'tr'),
title = domlib.newAt(tr, 'th');
title.innerHTML = 'Nodes';
title.setAttribute('colspan', '2');
this.nodes = domlib.newAt(tr, 'td');
tr = domlib.newAt(table, 'tr');
title = domlib.newAt(tr, 'th');
title.innerHTML = 'Clients';
title.setAttribute('colspan', '2');
this.clients = domlib.newAt(tr, 'td');
tr = domlib.newAt(table, 'tr');
tr.classList.add('line');
domlib.newAt(tr, 'th').innerHTML = 'Wifi';
domlib.newAt(tr, 'th').innerHTML = '2.4 Ghz';
domlib.newAt(tr, 'th').innerHTML = '5 Ghz';
tr = domlib.newAt(table, 'tr');
this.clientsWifi = domlib.newAt(tr, 'td');
this.clientsWifi24 = domlib.newAt(tr, 'td');
this.clientsWifi5 = domlib.newAt(tr, 'td');
// Channels table
domlib.newAt(this.el, 'h1').innerHTML = 'Channels';
this.channelTabelle = domlib.newAt(this.el, 'table');
this.channelTabelle.classList.add('stats');
socket.setEvent('stats', (msg) => {
if (msg.body) {
this.nodes.innerHTML = msg.body.Nodes;
this.clients.innerHTML = msg.body.Clients;
this.clientsWifi.innerHTML = msg.body.ClientsWifi;
this.clientsWifi24.innerHTML = msg.body.ClientsWifi24;
this.clientsWifi5.innerHTML = msg.body.ClientsWifi5;
while(this.channelTabelle.hasChildNodes()) {
this.channelTabelle.removeChild(this.channelTabelle.firstElementChild);
}
let tr = domlib.newAt(this.channelTabelle, 'tr');
let title = domlib.newAt(tr, 'th');
title.innerHTML = '2.4 Ghz';
title.setAttribute('colspan', '2');
title = domlib.newAt(tr, 'th');
title.innerHTML = '5 Ghz';
title.setAttribute('colspan', '2');
const storeNodes = store.getNodes();
for (let ch = 1; ch <= 33; ch++) {
tr = domlib.newAt(this.channelTabelle, 'tr');
if (ch < 14) {
domlib.newAt(tr, 'td').innerHTML = ch;
domlib.newAt(tr, 'td').innerHTML = storeNodes.reduce((c, node) => node.wireless.channel24 === ch ? c + 1 : c, 0);
} else {
domlib.newAt(tr, 'td');
domlib.newAt(tr, 'td');
}
const ch5 = 32 + ch * 4;
domlib.newAt(tr, 'td').innerHTML = ch5;
domlib.newAt(tr, 'td').innerHTML = storeNodes.reduce((c, node) => node.wireless.channel5 === ch5 ? c + 1 : c, 0);
}
}
});
}
}

File diff suppressed because it is too large Load Diff

40
webroot/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "freifunkmanager",
"version": "1.0.0",
"main": "index.js",
"license": "GPL-3.0",
"dependencies": {
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-es2017": "^6.24.1",
"babel-register": "^6.26.0",
"babelify": "^8.0.0",
"browser-sync": "^2.18.13",
"browserify": "^14.5.0",
"gulp": "3.9.0",
"gulp-autoprefixer": "^4.0.0",
"gulp-cli": "^1.4.0",
"gulp-less": "^3.3.2",
"gulp-load-plugins": "^1.5.0",
"gulp-plumber": "^1.1.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"leaflet": "^1.3.1",
"leaflet-ajax": "^2.1.0",
"leaflet-webgl-heatmap": "^0.2.7",
"navigo": "^5.3.3",
"semantic-ui-less": "^2.2.12",
"superfine": "^5.0.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.9.0"
},
"scripts": {
"gulp": "gulp"
},
"babel": {
"presets": [
"es2017"
]
}
}

5272
webroot/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,123 +0,0 @@
package websocket
import (
"io"
log "github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
)
const channelBufSize = 100
type Client struct {
ip string
ws *websocket.Conn
ch chan *Message
writeQuit chan bool
readQuit chan bool
}
func NewClient(ip string, ws *websocket.Conn) *Client {
if ws == nil {
log.Panic("ws cannot be nil")
}
return &Client{
ws: ws,
ch: make(chan *Message, channelBufSize),
writeQuit: make(chan bool),
readQuit: make(chan bool),
ip: ip,
}
}
func (c *Client) Write(msg *Message) {
select {
case c.ch <- msg:
default:
clientsMutex.Lock()
delete(clients, c.ip)
clientsMutex.Unlock()
log.Error("client disconnected")
}
}
func (c *Client) Close() {
c.writeQuit <- true
c.readQuit <- true
log.Info("client disconnecting...")
}
// Listen Write and Read request via chanel
func (c *Client) Listen() {
go c.listenWrite()
if stats != nil {
c.Write(&Message{Type: MessageTypeStats, Body: stats})
}
c.publishAllData()
c.listenRead()
}
func (c *Client) publishAllData() {
for _, node := range nodes.List {
c.Write(&Message{Type: MessageTypeSystemNode, Node: node})
}
for _, node := range nodes.Current {
c.Write(&Message{Type: MessageTypeCurrentNode, Node: node})
}
}
func (c *Client) handleMessage(msg *Message) {
switch msg.Type {
case MessageTypeSystemNode:
nodes.UpdateNode(msg.Node)
break
}
}
// Listen write request via chanel
func (c *Client) listenWrite() {
for {
select {
case msg := <-c.ch:
websocket.JSON.Send(c.ws, msg)
case <-c.writeQuit:
clientsMutex.Lock()
close(c.ch)
close(c.writeQuit)
delete(clients, c.ip)
clientsMutex.Unlock()
return
}
}
}
// Listen read request via chanel
func (c *Client) listenRead() {
for {
select {
case <-c.readQuit:
clientsMutex.Lock()
close(c.readQuit)
delete(clients, c.ip)
clientsMutex.Unlock()
return
default:
var msg Message
err := websocket.JSON.Receive(c.ws, &msg)
if err == io.EOF {
close(c.readQuit)
c.writeQuit <- true
return
} else if err != nil {
log.Error(err)
} else {
c.handleMessage(&msg)
}
}
}
}

27
websocket/handler.go Normal file
View File

@ -0,0 +1,27 @@
package websocket
import (
log "github.com/sirupsen/logrus"
"github.com/genofire/golang-lib/websocket"
)
type WebsocketHandlerFunc func(*log.Entry, *websocket.Message) error
func (ws *WebsocketServer) MessageHandler() {
for msg := range ws.inputMSG {
logger := log.WithFields(log.Fields{
"session": msg.Session,
"id": msg.ID,
"subject": msg.Subject,
})
if handler, ok := ws.handlers[msg.Subject]; ok {
err := handler(logger, msg)
if err != nil {
logger.Errorf("websocket message '%s' cound not handle: %s", msg.Subject, err)
}
} else {
logger.Warnf("websocket message '%s' cound not handle", msg.Subject)
}
}
}

55
websocket/hd_auth.go Normal file
View File

@ -0,0 +1,55 @@
package websocket
import (
log "github.com/sirupsen/logrus"
"github.com/genofire/golang-lib/websocket"
)
func (ws *WebsocketServer) loginHandler(logger *log.Entry, msg *websocket.Message) error {
_, ok := ws.loggedIn[msg.Session]
if ok {
msg.Answer(msg.Subject, true)
logger.Warn("already loggedIn")
return nil
}
secret, ok := msg.Body.(string)
if !ok {
logger.Warn("invalid secret format")
msg.Answer(msg.Subject, false)
return nil
}
ok = (ws.secret == secret)
if ok {
ws.loggedIn[msg.Session] = true
logger.Debug("done")
} else {
logger.Warn("wrong secret")
}
msg.Answer(msg.Subject, ok)
return nil
}
func (ws *WebsocketServer) authStatusHandler(logger *log.Entry, msg *websocket.Message) error {
login, ok := ws.loggedIn[msg.Session]
defer logger.Debug("done")
if !ok {
msg.Answer(msg.Subject, false)
return nil
}
msg.Answer(msg.Subject, login)
return nil
}
func (ws *WebsocketServer) logoutHandler(logger *log.Entry, msg *websocket.Message) error {
_, ok := ws.loggedIn[msg.Session]
if !ok {
msg.Answer(msg.Subject, false)
logger.Warn("logout without login")
return nil
}
ws.loggedIn[msg.Session] = false
delete(ws.loggedIn, msg.Session)
logger.Debug("done")
msg.Answer(msg.Subject, true)
return nil
}

20
websocket/hd_connect.go Normal file
View File

@ -0,0 +1,20 @@
package websocket
import (
log "github.com/sirupsen/logrus"
wsLib "github.com/genofire/golang-lib/websocket"
)
func (ws *WebsocketServer) connectHandler(logger *log.Entry, msg *wsLib.Message) error {
msg.From.Write(&wsLib.Message{Subject: MessageTypeStats, Body: ws.nodes.Statistics})
for _, node := range ws.nodes.List {
msg.From.Write(&wsLib.Message{Subject: MessageTypeSystemNode, Body: node})
}
for _, node := range ws.nodes.Current {
msg.From.Write(&wsLib.Message{Subject: MessageTypeCurrentNode, Body: node})
}
logger.Debug("done")
return nil
}

34
websocket/hd_node.go Normal file
View File

@ -0,0 +1,34 @@
package websocket
import (
"github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
wsLib "github.com/genofire/golang-lib/websocket"
"github.com/FreifunkBremen/freifunkmanager/runtime"
)
func (ws *WebsocketServer) nodeHandler(logger *log.Entry, msg *wsLib.Message) error {
if ok, exists := ws.loggedIn[msg.Session]; !ok || !exists {
msg.Answer(msg.Subject, false)
logger.Warn("not logged in")
return nil
}
node := runtime.Node{}
if err := mapstructure.Decode(msg.Body, &node); err != nil {
msg.Answer(msg.Subject, false)
logger.Warnf("not able to decode data: %s", err)
return nil
}
if node.NodeID == "" {
msg.Answer(msg.Subject, false)
logger.Warnf("not able to find nodeid")
logger.Debugf("%v", node)
return nil
}
ws.nodes.UpdateNode(&node)
msg.Answer(msg.Subject, true)
logger.Infof("change %s", node.NodeID)
return nil
}

View File

@ -1,15 +1,13 @@
package websocket package websocket
import "github.com/FreifunkBremen/freifunkmanager/runtime"
type Message struct {
Type string `json:"type"`
Body interface{} `json:"body,omitempty"`
Node *runtime.Node `json:"node,omitempty"`
}
const ( const (
MessageTypeSystemNode = "system" MessageTypeConnect = "connect"
MessageTypeCurrentNode = "current"
MessageTypeLogin = "login"
MessageTypeAuthStatus = "auth_status"
MessageTypeLogout = "logout"
MessageTypeSystemNode = "node-system"
MessageTypeCurrentNode = "node-current"
MessageTypeStats = "stats" MessageTypeStats = "stats"
) )

21
websocket/send.go Normal file
View File

@ -0,0 +1,21 @@
package websocket
import (
wsLib "github.com/genofire/golang-lib/websocket"
yanicRuntime "github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/freifunkmanager/runtime"
)
func (ws *WebsocketServer) SendNode(node *runtime.Node, system bool) {
msgType := MessageTypeCurrentNode
if system {
msgType = MessageTypeSystemNode
}
ws.ws.SendAll(&wsLib.Message{Subject: msgType, Body: node})
}
func (ws *WebsocketServer) SendStats(data *yanicRuntime.GlobalStats) {
ws.ws.SendAll(&wsLib.Message{Subject: MessageTypeStats, Body: data})
}

View File

@ -2,69 +2,47 @@ package websocket
import ( import (
"net/http" "net/http"
"sync"
runtimeYanic "github.com/FreifunkBremen/yanic/runtime" wsLib "github.com/genofire/golang-lib/websocket"
httpLib "github.com/genofire/golang-lib/http" "github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
"github.com/FreifunkBremen/freifunkmanager/runtime" "github.com/FreifunkBremen/freifunkmanager/runtime"
) )
var nodes *runtime.Nodes type WebsocketServer struct {
var clients map[string]*Client nodes *runtime.Nodes
var clientsMutex sync.Mutex secret string
var stats *runtimeYanic.GlobalStats loggedIn map[uuid.UUID]bool
func Start(nodeBind *runtime.Nodes) { inputMSG chan *wsLib.Message
nodes = nodeBind ws *wsLib.Server
clients = make(map[string]*Client) handlers map[string]WebsocketHandlerFunc
http.Handle("/websocket", websocket.Handler(func(ws *websocket.Conn) {
r := ws.Request()
ip := httpLib.GetRemoteIP(r)
defer func() {
ws.Close()
clientsMutex.Lock()
delete(clients, ip)
clientsMutex.Unlock()
log.Info("client disconnected")
}()
log.Infof("new client")
client := NewClient(ip, ws)
clientsMutex.Lock()
clients[ip] = client
clientsMutex.Unlock()
client.Listen()
}))
nodes.AddNotify(NotifyNode)
} }
func NotifyNode(node *runtime.Node, system bool) { func NewWebsocketServer(secret string, nodes *runtime.Nodes) *WebsocketServer {
msgType := MessageTypeCurrentNode ownWS := WebsocketServer{
if system { nodes: nodes,
msgType = MessageTypeSystemNode handlers: make(map[string]WebsocketHandlerFunc),
loggedIn: make(map[uuid.UUID]bool),
inputMSG: make(chan *wsLib.Message),
secret: secret,
} }
SendAll(Message{Type: msgType, Node: node}) ownWS.ws = wsLib.NewServer(ownWS.inputMSG, wsLib.NewSessionManager())
}
func NotifyStats(data *runtimeYanic.GlobalStats) { // Register Handlers
stats = data ownWS.handlers[MessageTypeConnect] = ownWS.connectHandler
SendAll(Message{Type: MessageTypeStats, Body: data})
} ownWS.handlers[MessageTypeLogin] = ownWS.loginHandler
func SendAll(msg Message) { ownWS.handlers[MessageTypeAuthStatus] = ownWS.authStatusHandler
clientsMutex.Lock() ownWS.handlers[MessageTypeLogout] = ownWS.logoutHandler
for _, c := range clients {
c.Write(&msg) ownWS.handlers[MessageTypeSystemNode] = ownWS.nodeHandler
}
clientsMutex.Unlock() http.HandleFunc("/ws", ownWS.ws.Handler)
go ownWS.MessageHandler()
return &ownWS
} }
func Close() { func (ws *WebsocketServer) Close() {
log.Infof("websocket stopped with %d clients", len(clients)) close(ws.inputMSG)
} }

View File

@ -1,17 +0,0 @@
package websocket
import (
"testing"
"github.com/FreifunkBremen/freifunkmanager/runtime"
"github.com/stretchr/testify/assert"
)
func TestStart(t *testing.T) {
assert := assert.New(t)
nodes := &runtime.Nodes{}
assert.Nil(clients)
Start(nodes)
assert.NotNil(clients)
Close()
}