cleanup and migrate to newer libraries (use NEW Javascript env) + add secret
This commit is contained in:
parent
a86ac56b75
commit
82270e6fa3
|
@ -124,12 +124,6 @@ __pycache__
|
|||
# IDE's go
|
||||
.idea/
|
||||
|
||||
# webroot
|
||||
webroot/node_modules
|
||||
webroot/data
|
||||
webroot/app.js
|
||||
webroot/app.js.map
|
||||
|
||||
# go project
|
||||
profile.cov
|
||||
config.conf
|
||||
|
|
|
@ -3,6 +3,7 @@ state_path = "/tmp/freifunkmanager.json"
|
|||
webserver_bind = ":8080"
|
||||
webroot = "./webroot"
|
||||
|
||||
secret = "passw0rd"
|
||||
|
||||
ssh_key = "~/.ssh/id_rsa"
|
||||
ssh_interface = "wlp4s0"
|
||||
|
|
11
main.go
11
main.go
|
@ -54,8 +54,9 @@ func main() {
|
|||
go nodesSaveWorker.Start()
|
||||
go nodesUpdateWorker.Start()
|
||||
|
||||
websocket.Start(nodes)
|
||||
db.NotifyStats = websocket.NotifyStats
|
||||
ws := websocket.NewWebsocketServer(config.Secret, nodes)
|
||||
nodes.AddNotifyStats(ws.SendStats)
|
||||
nodes.AddNotifyNode(ws.SendNode)
|
||||
|
||||
if config.Yanic.Enable {
|
||||
collector = respondYanic.NewCollector(db, nodesYanic, make(map[string][]string), []respondYanic.InterfaceConfig{respondYanic.InterfaceConfig{
|
||||
|
@ -71,7 +72,7 @@ func main() {
|
|||
httpLib.Write(w, nodes)
|
||||
})
|
||||
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))))
|
||||
|
||||
|
@ -80,7 +81,7 @@ func main() {
|
|||
}
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
|
@ -92,7 +93,7 @@ func main() {
|
|||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
|
||||
websocket.Close()
|
||||
ws.Close()
|
||||
|
||||
// Stop services
|
||||
srv.Close()
|
||||
|
|
|
@ -10,6 +10,9 @@ type Config struct {
|
|||
// path to deliver static content
|
||||
Webroot string `toml:"webroot"`
|
||||
|
||||
// auth secret
|
||||
Secret string `toml:"secret"`
|
||||
|
||||
// SSH private key
|
||||
SSHPrivateKey string `toml:"ssh_key"`
|
||||
SSHInterface string `toml:"ssh_interface"`
|
||||
|
|
|
@ -2,38 +2,25 @@ package runtime
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
yanicData "github.com/FreifunkBremen/yanic/data"
|
||||
"github.com/FreifunkBremen/yanic/lib/jsontime"
|
||||
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 {
|
||||
Lastseen jsontime.Time `json:"lastseen"`
|
||||
NodeID string `json:"node_id"`
|
||||
Lastseen jsontime.Time `json:"lastseen" mapstructure:"-"`
|
||||
NodeID string `json:"node_id" mapstructure:"node_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Location yanicData.Location `json:"location"`
|
||||
Wireless yanicData.Wireless `json:"wireless"`
|
||||
Owner string `json:"owner"`
|
||||
Address net.IP `json:"-"`
|
||||
Address net.IP `json:"ip" mapstructure:"-"`
|
||||
Stats struct {
|
||||
Wireless yanicData.WirelessStatistics `json:"wireless"`
|
||||
Clients yanicData.Clients `json:"clients"`
|
||||
} `json:"statistics"`
|
||||
} `json:"statistics" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func NewNode(nodeOrigin *yanicRuntime.Node) *Node {
|
||||
|
@ -65,28 +52,6 @@ func NewNode(nodeOrigin *yanicRuntime.Node) *Node {
|
|||
}
|
||||
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 {
|
||||
return net.TCPAddr{IP: n.Address, Port: 22, Zone: iface}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,10 +3,9 @@ package runtime
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/FreifunkBremen/yanic/lib/jsontime"
|
||||
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/FreifunkBremen/freifunkmanager/ssh"
|
||||
|
@ -15,10 +14,12 @@ import (
|
|||
type Nodes struct {
|
||||
List map[string]*Node `json:"nodes"`
|
||||
Current map[string]*Node `json:"-"`
|
||||
Statistics *runtimeYanic.GlobalStats `json:"-"`
|
||||
ssh *ssh.Manager
|
||||
statePath string
|
||||
iface string
|
||||
notifyFunc []func(*Node, bool)
|
||||
notifyNodeFunc []func(*Node, bool)
|
||||
notifyStatsFunc []func(*runtimeYanic.GlobalStats)
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
|
@ -34,53 +35,25 @@ func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes {
|
|||
return nodes
|
||||
}
|
||||
|
||||
func (nodes *Nodes) LearnNode(n *yanicRuntime.Node) {
|
||||
node := NewNode(n)
|
||||
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) AddNotifyNode(f func(*Node, bool)) {
|
||||
nodes.notifyNodeFunc = append(nodes.notifyNodeFunc, f)
|
||||
}
|
||||
|
||||
func (nodes *Nodes) AddNotify(f func(*Node, bool)) {
|
||||
nodes.notifyFunc = append(nodes.notifyFunc, f)
|
||||
}
|
||||
func (nodes *Nodes) notify(node *Node, system bool) {
|
||||
for _, f := range nodes.notifyFunc {
|
||||
func (nodes *Nodes) notifyNode(node *Node, system bool) {
|
||||
for _, f := range nodes.notifyNodeFunc {
|
||||
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) {
|
||||
if node == nil {
|
||||
log.Warn("no new node to update")
|
||||
|
@ -94,7 +67,7 @@ func (nodes *Nodes) UpdateNode(node *Node) {
|
|||
log.Info("update node", node.NodeID)
|
||||
}
|
||||
nodes.List[node.NodeID] = node
|
||||
nodes.notify(node, true)
|
||||
nodes.notifyNode(node, true)
|
||||
}
|
||||
|
||||
func (nodes *Nodes) Updater() {
|
||||
|
|
|
@ -3,15 +3,18 @@ package runtime
|
|||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
databaseYanic "github.com/FreifunkBremen/yanic/database"
|
||||
"github.com/FreifunkBremen/yanic/lib/jsontime"
|
||||
runtimeYanic "github.com/FreifunkBremen/yanic/runtime"
|
||||
|
||||
"github.com/FreifunkBremen/freifunkmanager/ssh"
|
||||
)
|
||||
|
||||
type YanicDB struct {
|
||||
databaseYanic.Connection
|
||||
nodes *Nodes
|
||||
Statistics *runtimeYanic.GlobalStats
|
||||
NotifyStats func(data *runtimeYanic.GlobalStats)
|
||||
}
|
||||
|
||||
func NewYanicDB(nodes *Nodes) *YanicDB {
|
||||
|
@ -20,18 +23,52 @@ func NewYanicDB(nodes *Nodes) *YanicDB {
|
|||
}
|
||||
}
|
||||
|
||||
func (conn *YanicDB) InsertNode(node *runtimeYanic.Node) {
|
||||
conn.nodes.LearnNode(node)
|
||||
func (conn *YanicDB) InsertNode(n *runtimeYanic.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) InsertGlobals(stats *runtimeYanic.GlobalStats, time time.Time, site string, domain string) {
|
||||
conn.Statistics = stats
|
||||
if conn.NotifyStats != nil {
|
||||
conn.NotifyStats(stats)
|
||||
if runtimeYanic.GLOBAL_SITE == site && runtimeYanic.GLOBAL_DOMAIN == domain {
|
||||
conn.nodes.notifyStats(stats)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (conn *YanicDB) PruneNodes(deleteAfter time.Duration) {
|
||||
|
|
|
@ -14,10 +14,10 @@ func (m *Manager) ExecuteOn(addr net.TCPAddr, cmd string) error {
|
|||
return err
|
||||
}
|
||||
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()
|
||||
defer session.Close()
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ func (m *Manager) RunOn(addr net.TCPAddr, cmd string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
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()
|
||||
defer session.Close()
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["env"]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
data
|
||||
app.js
|
||||
app.js.map
|
||||
styles.css
|
||||
styles.css.map
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
|
@ -6,20 +13,6 @@ body {
|
|||
line-height: 1.3;
|
||||
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 {
|
||||
color: #009ee0;
|
||||
}
|
||||
|
@ -29,85 +22,6 @@ span.offline {
|
|||
h1 {
|
||||
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 {
|
||||
font-size: 1.3em;
|
|
@ -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'
|
||||
]);
|
|
@ -10,42 +10,10 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<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>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<script src="/app.js"></script>
|
||||
</head>
|
||||
<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>
|
||||
<strong>JavaScript required</strong>
|
||||
</noscript>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/* exported config */
|
||||
|
||||
/* eslint no-magic-numbers: "off"*/
|
||||
/* eslint sort-keys: "off"*/
|
||||
|
||||
const config = {
|
||||
export default {
|
||||
'title': 'FreifunkManager - Breminale',
|
||||
'backend': `ws${location.protocol == 'https:' ? 's' : ''}://${location.host}/websocket`,
|
||||
'backend': `ws${location.protocol == 'https:' ? 's' : ''}://${location.host}/ws`,
|
||||
'node': {
|
||||
// Minuten till is shown as offline
|
||||
'offline': 5
|
||||
|
@ -20,22 +18,16 @@ const config = {
|
|||
},
|
||||
'maxZoom': 20,
|
||||
'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': {
|
||||
'wifi24': {
|
||||
'size': 30,
|
||||
'size': 10,
|
||||
'units': 'm',
|
||||
'opacity': 0.5,
|
||||
'alphaRange': 1
|
||||
},
|
||||
'wifi5': {
|
||||
'size': 30,
|
||||
'size': 10,
|
||||
'units': 'm',
|
||||
'opacity': 0.5,
|
||||
'alphaRange': 1
|
||||
}
|
||||
|
@ -51,7 +43,7 @@ const config = {
|
|||
}
|
||||
},
|
||||
'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) {
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
(function init () {
|
||||
'use strict';
|
||||
|
||||
domlib.newAt = function newAt (at, eltype) {
|
||||
export function newEl (eltype, props, content) {
|
||||
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);
|
||||
|
||||
return el;
|
||||
};
|
||||
domlib.removeChildren = function removeChildren (el) {
|
||||
if (el) {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.firstChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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};
|
|
@ -1,101 +1,43 @@
|
|||
/* exported gui,router */
|
||||
/* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode */
|
||||
import * as domlib from './domlib';
|
||||
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 = {},
|
||||
router = new Navigo(null, true, '#');
|
||||
const router = new Navigo(null, true, '#'),
|
||||
elMain = domlib.newEl('main'),
|
||||
elMenu = new MenuView();
|
||||
|
||||
export {router};
|
||||
|
||||
(function init () {
|
||||
'use strict';
|
||||
|
||||
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);
|
||||
let init = false,
|
||||
currentView = new View();
|
||||
|
||||
export function render () {
|
||||
if (!document.body) {
|
||||
return;
|
||||
}
|
||||
status.classList.remove('connecting', 'offline');
|
||||
if (socket.readyState !== 1) {
|
||||
let statusClass = 'offline';
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (socket.readyState === 0 || socket.readyState === 2) {
|
||||
statusClass = 'connecting';
|
||||
}
|
||||
status.classList.add(statusClass);
|
||||
}
|
||||
if (!init) {
|
||||
elMenu.bind(document.body);
|
||||
notify.bind(document.body);
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
notify.bind(document.getElementsByClassName('notifications')[0]);
|
||||
document.body.appendChild(elMain);
|
||||
|
||||
init = true;
|
||||
}
|
||||
|
||||
currentView.render();
|
||||
|
||||
notify.render();
|
||||
elMenu.render();
|
||||
|
||||
router.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function setView (toView) {
|
||||
export function setView (toView) {
|
||||
currentView.unbind();
|
||||
currentView = toView;
|
||||
const main = document.querySelector('main');
|
||||
|
||||
domlib.removeChildren(main);
|
||||
currentView.bind(main);
|
||||
currentView.bind(elMain);
|
||||
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;
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
})();
|
|
@ -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();
|
||||
};
|
||||
})();
|
|
@ -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();
|
||||
};
|
||||
})();
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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);
|
||||
};
|
||||
})();
|
|
@ -1,89 +1,200 @@
|
|||
/* exported socket */
|
||||
/* globals notify,gui,store,config*/
|
||||
let socket = {'readyState': 0};
|
||||
import * as store from './store';
|
||||
import config from './config';
|
||||
import {singelton as notify} from './element/notify';
|
||||
import {render} from './gui';
|
||||
|
||||
(function init () {
|
||||
'use strict';
|
||||
const RECONNECT_AFTER = 5000,
|
||||
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);
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (socket.readyState !== 3) {
|
||||
notify.send('error', 'Es gibt Übertragungsprobleme!');
|
||||
gui.render();
|
||||
notify.send({
|
||||
'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 () {
|
||||
gui.render();
|
||||
if (msgFunc) {
|
||||
msgFunc(msg);
|
||||
delete eventMSGID[msg.id];
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
function onmessage (raw) {
|
||||
const msg = JSON.parse(raw.data);
|
||||
|
||||
switch (msg.type) {
|
||||
case 'system':
|
||||
store.updateNode(msg.node, true);
|
||||
break;
|
||||
case 'current':
|
||||
store.updateNode(msg.node);
|
||||
break;
|
||||
case 'stats':
|
||||
if (msg.body) {
|
||||
store.stats = msg.body;
|
||||
if (typeof eventFuncs === 'object' && eventFuncs.length > 0) {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in eventFuncs) {
|
||||
const func = eventFuncs[key];
|
||||
if (func) {
|
||||
func(msg);
|
||||
}
|
||||
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');
|
||||
notify.send('warn', 'Es besteht ein Verbindungsproblem!');
|
||||
gui.render();
|
||||
notify.send({
|
||||
'header': 'Verbindung',
|
||||
'type': 'warning'
|
||||
}, 'Verbindung zum Server beendet!');
|
||||
render();
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
window.setTimeout(connect, RECONNECT_AFTER);
|
||||
}
|
||||
}
|
||||
|
||||
function sendnode (node) {
|
||||
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 () {
|
||||
function connect () {
|
||||
socket = new window.WebSocket(config.backend);
|
||||
socket.onopen = onopen;
|
||||
socket.onerror = onerror;
|
||||
socket.onmessage = onmessage;
|
||||
socket.onclose = onclose;
|
||||
socket.sendnode = sendnode;
|
||||
socket.sendcmd = sendcmd;
|
||||
}
|
||||
sendjson({'subject': 'auth_status'});
|
||||
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();
|
||||
|
|
|
@ -1,27 +1,7 @@
|
|||
/* exported store */
|
||||
const current = {},
|
||||
list = {};
|
||||
|
||||
|
||||
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) {
|
||||
export function getNode (nodeid) {
|
||||
let node = {};
|
||||
|
||||
if (list[nodeid]) {
|
||||
|
@ -42,28 +22,16 @@ const store = {
|
|||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
store.updateNode = function updateNode (node, system) {
|
||||
export function updateNode (node, system) {
|
||||
if (system) {
|
||||
list[node.node_id] = node;
|
||||
} else {
|
||||
current[node.node_id] = node;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
store.getNode = getNode;
|
||||
|
||||
store.getNodes = function getNodes () {
|
||||
export function getNodes () {
|
||||
return Object.keys(list).map(getNode);
|
||||
};
|
||||
|
||||
store.updateCMD = function updateCMD (cmd) {
|
||||
cmds[cmd.id] = cmd;
|
||||
};
|
||||
|
||||
store.getCMDs = function getCMDs () {
|
||||
return cmds;
|
||||
};
|
||||
})();
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -1,32 +1,130 @@
|
|||
/* exported guiList */
|
||||
/* global config,domlib,store,router,socket */
|
||||
/* eslint max-lines: [off] */
|
||||
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 {FromNowAgo} from '../lib';
|
||||
|
||||
const guiList = {};
|
||||
export class ListView extends View {
|
||||
|
||||
(function init () {
|
||||
'use strict';
|
||||
constructor () {
|
||||
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,
|
||||
el = null,
|
||||
tbody = null,
|
||||
sortReverse = false,
|
||||
sortIndex = null,
|
||||
hostnameFilter = null,
|
||||
nodeidFilter = null,
|
||||
editing = false;
|
||||
// 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', () => {
|
||||
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
|
||||
function sort (a, b) {
|
||||
sort (sortIndex, sortReverse) {
|
||||
function sortNumber (aNum, bNum) {
|
||||
return aNum - bNum;
|
||||
}
|
||||
return (a, b) => {
|
||||
if (!sortIndex) {
|
||||
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':
|
||||
return a.lastseen - b.lastseen;
|
||||
case 'CurPower':
|
||||
|
@ -43,14 +141,12 @@ const guiList = {};
|
|||
return a.statistics.clients.wifi24 - b.statistics.clients.wifi24;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
case 'ChanUtil':
|
||||
if(a.statistics.wireless === null) return 1;
|
||||
if(b.statistics.wireless === null) return -1;
|
||||
// eslint-disable-next-line id-length
|
||||
let aMax = a.statistics.wireless.map((d) =>
|
||||
d.ChanUtil
|
||||
).sort(sortNumber),
|
||||
let aMax = a.statistics.wireless.map((d) => d.ChanUtil).sort(sortNumber),
|
||||
// eslint-disable-next-line id-length
|
||||
bMax = b.statistics.wireless.map((d) =>
|
||||
d.ChanUtil
|
||||
).sort(sortNumber);
|
||||
bMax = b.statistics.wireless.map((d) => d.ChanUtil).sort(sortNumber);
|
||||
|
||||
if (!sortReverse) {
|
||||
aMax = aMax.reverse();
|
||||
|
@ -58,14 +154,13 @@ const guiList = {};
|
|||
}
|
||||
|
||||
return bMax[0] - aMax[0];
|
||||
case 'Hostname':
|
||||
return a.hostname.localeCompare(b.hostname);
|
||||
default:
|
||||
return a.node_id.localeCompare(b.node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderRow (node) {
|
||||
renderRow (node) {
|
||||
const startdate = new Date(),
|
||||
tr = document.createElement('tr'),
|
||||
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;
|
||||
|
||||
|
@ -103,17 +198,23 @@ const guiList = {};
|
|||
hostnameInput.readOnly = true;
|
||||
hostnameInput.setAttribute('placeholder', 'Hostname');
|
||||
hostnameInput.addEventListener('dblclick', () => {
|
||||
editing = true;
|
||||
this.editing = true;
|
||||
hostnameInput.readOnly = false;
|
||||
});
|
||||
hostnameInput.addEventListener('focusout', () => {
|
||||
if (hostnameInput.readOnly) {
|
||||
return;
|
||||
}
|
||||
editing = false;
|
||||
this.editing = false;
|
||||
hostnameInput.readOnly = true;
|
||||
const old = node.hostname;
|
||||
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';
|
||||
|
@ -134,17 +235,23 @@ const guiList = {};
|
|||
channel24Input.readOnly = true;
|
||||
channel24Input.setAttribute('placeholder', '-');
|
||||
channel24Input.addEventListener('dblclick', () => {
|
||||
editing = true;
|
||||
this.editing = true;
|
||||
channel24Input.readOnly = false;
|
||||
});
|
||||
channel24Input.addEventListener('focusout', () => {
|
||||
if (channel24Input.readOnly) {
|
||||
return;
|
||||
}
|
||||
editing = false;
|
||||
this.editing = false;
|
||||
channel24Input.readOnly = true;
|
||||
const old = node.wireless.channel24;
|
||||
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 || '';
|
||||
|
@ -155,17 +262,23 @@ const guiList = {};
|
|||
channel5Input.readOnly = true;
|
||||
channel5Input.setAttribute('placeholder', '-');
|
||||
channel5Input.addEventListener('dblclick', () => {
|
||||
editing = true;
|
||||
this.editing = true;
|
||||
channel5Input.readOnly = false;
|
||||
});
|
||||
channel5Input.addEventListener('focusout', () => {
|
||||
if (channel5Input.readOnly) {
|
||||
return;
|
||||
}
|
||||
editing = false;
|
||||
this.editing = false;
|
||||
channel5Input.readOnly = true;
|
||||
const old = node.wireless.channel5;
|
||||
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 */
|
||||
|
@ -182,17 +295,23 @@ const guiList = {};
|
|||
power24Input.readOnly = true;
|
||||
power24Input.setAttribute('placeholder', '-');
|
||||
power24Input.addEventListener('dblclick', () => {
|
||||
editing = true;
|
||||
this.editing = true;
|
||||
power24Input.readOnly = false;
|
||||
});
|
||||
power24Input.addEventListener('focusout', () => {
|
||||
if (power24Input.readOnly) {
|
||||
return;
|
||||
}
|
||||
editing = false;
|
||||
this.editing = false;
|
||||
power24Input.readOnly = true;
|
||||
const old = node.wireless.txpower24;
|
||||
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 || '';
|
||||
|
@ -202,17 +321,23 @@ const guiList = {};
|
|||
power5Input.readOnly = true;
|
||||
power5Input.setAttribute('placeholder', '-');
|
||||
power5Input.addEventListener('dblclick', () => {
|
||||
editing = true;
|
||||
this.editing = true;
|
||||
power5Input.readOnly = false;
|
||||
});
|
||||
power5Input.addEventListener('focusout', () => {
|
||||
if (power5Input.readOnly) {
|
||||
return;
|
||||
}
|
||||
editing = false;
|
||||
this.editing = false;
|
||||
power5Input.readOnly = true;
|
||||
const old = node.wireless.txpower5;
|
||||
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;
|
||||
|
@ -233,173 +358,58 @@ const guiList = {};
|
|||
edit.classList.add('btn');
|
||||
edit.innerHTML = 'Edit';
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
domlib.removeChildren(tbody);
|
||||
while(this.tbody.hasChildNodes()) {
|
||||
this.tbody.removeChild(this.tbody.firstElementChild);
|
||||
}
|
||||
let nodes = store.getNodes();
|
||||
|
||||
if (hostnameFilter && hostnameFilter.value !== '') {
|
||||
if (this.hostnameFilter && this.hostnameFilter.value !== '') {
|
||||
// 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
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
})();
|
||||
}
|
|
@ -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,
|
||||
WINDOW_HEIGHT_MENU = 50;
|
||||
export class MapView extends View {
|
||||
|
||||
let container = null,
|
||||
el = null,
|
||||
constructor () {
|
||||
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
|
||||
if(node.node_id === draggingNodeID){
|
||||
return
|
||||
|
@ -88,29 +116,32 @@ const guiMap = {};
|
|||
*/
|
||||
nodemarker.on('dragend', () => {
|
||||
// DraggingNodeID = undefined;
|
||||
const pos = nodemarker.getLatLng();
|
||||
const pos = nodemarker.getLatLng(),
|
||||
old = node.location;
|
||||
|
||||
node.location = {
|
||||
'latitude': pos.lat,
|
||||
'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 () {
|
||||
geoJsonLayer.refresh();
|
||||
nodeLayer.clearLayers();
|
||||
render () {
|
||||
this.geoJsonLayer.refresh();
|
||||
this.nodeLayer.clearLayers();
|
||||
|
||||
const nodes = store.getNodes();
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
addNode(nodes[i]);
|
||||
this.addNode(nodes[i]);
|
||||
}
|
||||
|
||||
|
||||
clientLayer24.setData(nodes.map((node) => {
|
||||
this.clientLayer24.setData(nodes.map((node) => {
|
||||
if (!node.location || !node.location.latitude || !node.location.longitude) {
|
||||
return null;
|
||||
}
|
||||
|
@ -118,59 +149,13 @@ const guiMap = {};
|
|||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
})();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
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 (
|
||||
MessageTypeSystemNode = "system"
|
||||
MessageTypeCurrentNode = "current"
|
||||
MessageTypeConnect = "connect"
|
||||
|
||||
MessageTypeLogin = "login"
|
||||
MessageTypeAuthStatus = "auth_status"
|
||||
MessageTypeLogout = "logout"
|
||||
|
||||
MessageTypeSystemNode = "node-system"
|
||||
MessageTypeCurrentNode = "node-current"
|
||||
MessageTypeStats = "stats"
|
||||
)
|
||||
|
|
|
@ -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})
|
||||
}
|
|
@ -2,69 +2,47 @@ package websocket
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
runtimeYanic "github.com/FreifunkBremen/yanic/runtime"
|
||||
httpLib "github.com/genofire/golang-lib/http"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/websocket"
|
||||
wsLib "github.com/genofire/golang-lib/websocket"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/FreifunkBremen/freifunkmanager/runtime"
|
||||
)
|
||||
|
||||
var nodes *runtime.Nodes
|
||||
var clients map[string]*Client
|
||||
var clientsMutex sync.Mutex
|
||||
var stats *runtimeYanic.GlobalStats
|
||||
type WebsocketServer struct {
|
||||
nodes *runtime.Nodes
|
||||
secret string
|
||||
loggedIn map[uuid.UUID]bool
|
||||
|
||||
func Start(nodeBind *runtime.Nodes) {
|
||||
nodes = nodeBind
|
||||
clients = make(map[string]*Client)
|
||||
|
||||
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)
|
||||
inputMSG chan *wsLib.Message
|
||||
ws *wsLib.Server
|
||||
handlers map[string]WebsocketHandlerFunc
|
||||
}
|
||||
|
||||
func NotifyNode(node *runtime.Node, system bool) {
|
||||
msgType := MessageTypeCurrentNode
|
||||
if system {
|
||||
msgType = MessageTypeSystemNode
|
||||
func NewWebsocketServer(secret string, nodes *runtime.Nodes) *WebsocketServer {
|
||||
ownWS := WebsocketServer{
|
||||
nodes: nodes,
|
||||
handlers: make(map[string]WebsocketHandlerFunc),
|
||||
loggedIn: make(map[uuid.UUID]bool),
|
||||
inputMSG: make(chan *wsLib.Message),
|
||||
secret: secret,
|
||||
}
|
||||
SendAll(Message{Type: msgType, Node: node})
|
||||
}
|
||||
func NotifyStats(data *runtimeYanic.GlobalStats) {
|
||||
stats = data
|
||||
SendAll(Message{Type: MessageTypeStats, Body: data})
|
||||
}
|
||||
func SendAll(msg Message) {
|
||||
clientsMutex.Lock()
|
||||
for _, c := range clients {
|
||||
c.Write(&msg)
|
||||
}
|
||||
clientsMutex.Unlock()
|
||||
ownWS.ws = wsLib.NewServer(ownWS.inputMSG, wsLib.NewSessionManager())
|
||||
|
||||
// Register Handlers
|
||||
ownWS.handlers[MessageTypeConnect] = ownWS.connectHandler
|
||||
|
||||
ownWS.handlers[MessageTypeLogin] = ownWS.loginHandler
|
||||
ownWS.handlers[MessageTypeAuthStatus] = ownWS.authStatusHandler
|
||||
ownWS.handlers[MessageTypeLogout] = ownWS.logoutHandler
|
||||
|
||||
ownWS.handlers[MessageTypeSystemNode] = ownWS.nodeHandler
|
||||
|
||||
http.HandleFunc("/ws", ownWS.ws.Handler)
|
||||
go ownWS.MessageHandler()
|
||||
return &ownWS
|
||||
}
|
||||
|
||||
func Close() {
|
||||
log.Infof("websocket stopped with %d clients", len(clients))
|
||||
func (ws *WebsocketServer) Close() {
|
||||
close(ws.inputMSG)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue