Browse Source

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

pull/3/head
Martin/Geno 4 years ago
parent
commit
82270e6fa3
No known key found for this signature in database GPG Key ID: 9D7D3C6BFF600C6A
  1. 6
      .gitignore
  2. 1
      config_example.conf
  3. 11
      main.go
  4. 3
      runtime/config.go
  5. 43
      runtime/node.go
  6. 105
      runtime/node_ssh.go
  7. 71
      runtime/nodes.go
  8. 53
      runtime/yanic.go
  9. 4
      ssh/execute.go
  10. 4
      ssh/run.go
  11. 3
      webroot/.babelrc
  12. 6
      webroot/.gitignore
  13. 16
      webroot/css/_login.less
  14. 0
      webroot/css/_map.less
  15. 61
      webroot/css/_menu.less
  16. 29
      webroot/css/_notify.less
  17. 25
      webroot/css/_status.less
  18. 62
      webroot/css/console.css
  19. BIN
      webroot/css/images/layers-2x.png
  20. BIN
      webroot/css/images/layers.png
  21. BIN
      webroot/css/images/marker-icon-2x.png
  22. BIN
      webroot/css/images/marker-icon.png
  23. BIN
      webroot/css/images/marker-shadow.png
  24. 624
      webroot/css/leaflet.css
  25. 100
      webroot/css/styles.less
  26. 94
      webroot/gulpfile.babel.js
  27. 36
      webroot/index.html
  28. 22
      webroot/js/config.js
  29. 66
      webroot/js/domlib.js
  30. 101
      webroot/js/element/menu.js
  31. 91
      webroot/js/element/notify.js
  32. 120
      webroot/js/gui.js
  33. 199
      webroot/js/gui_node.js
  34. 33
      webroot/js/gui_skelView.js
  35. 107
      webroot/js/gui_stats.js
  36. 44
      webroot/js/index.js
  37. 6
      webroot/js/leaflet-webgl-heatmap.min.js
  38. 1
      webroot/js/leaflet.ajax.min.js
  39. 9
      webroot/js/leaflet.js
  40. 25
      webroot/js/lib.js
  41. 7
      webroot/js/moment.js
  42. 2
      webroot/js/navigo.js
  43. 67
      webroot/js/notify.js
  44. 245
      webroot/js/socket.js
  45. 90
      webroot/js/store.js
  46. 22
      webroot/js/view.js
  47. 444
      webroot/js/view/list.js
  48. 133
      webroot/js/view/map.js
  49. 203
      webroot/js/view/node.js
  50. 84
      webroot/js/view/statistics.js
  51. 1040
      webroot/js/webgl-heatmap.js
  52. 40
      webroot/package.json
  53. 5272
      webroot/yarn.lock
  54. 123
      websocket/client.go
  55. 27
      websocket/handler.go
  56. 55
      websocket/hd_auth.go
  57. 20
      websocket/hd_connect.go
  58. 34
      websocket/hd_node.go
  59. 16
      websocket/msg.go
  60. 21
      websocket/send.go
  61. 82
      websocket/server.go
  62. 17
      websocket/server_test.go

6
.gitignore

@ -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

1
config_example.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

@ -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()

3
runtime/config.go

@ -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"`

43
runtime/node.go

@ -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}
}

105
runtime/node_ssh.go

@ -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
}

71
runtime/nodes.go

@ -3,22 +3,23 @@ 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"
)
type Nodes struct {
List map[string]*Node `json:"nodes"`
Current map[string]*Node `json:"-"`
ssh *ssh.Manager
statePath string
iface string
notifyFunc []func(*Node, bool)
List map[string]*Node `json:"nodes"`
Current map[string]*Node `json:"-"`
Statistics *runtimeYanic.GlobalStats `json:"-"`
ssh *ssh.Manager
statePath string
iface string
notifyNodeFunc []func(*Node, bool)
notifyStatsFunc []func(*runtimeYanic.GlobalStats)
sync.Mutex
}
@ -34,50 +35,22 @@ 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)
func (nodes *Nodes) AddNotifyNode(f func(*Node, bool)) {
nodes.notifyNodeFunc = append(nodes.notifyNodeFunc, f)
}
func (nodes *Nodes) notifyNode(node *Node, system bool) {
for _, f := range nodes.notifyNodeFunc {
f(node, system)
}
nodes.notify(node, false)
}
func (nodes *Nodes) AddNotify(f func(*Node, bool)) {
nodes.notifyFunc = append(nodes.notifyFunc, f)
func (nodes *Nodes) AddNotifyStats(f func(stats *runtimeYanic.GlobalStats)) {
nodes.notifyStatsFunc = append(nodes.notifyStatsFunc, f)
}
func (nodes *Nodes) notify(node *Node, system bool) {
for _, f := range nodes.notifyFunc {
f(node, system)
func (nodes *Nodes) notifyStats(stats *runtimeYanic.GlobalStats) {
nodes.Statistics = stats
for _, f := range nodes.notifyStatsFunc {
f(stats)
}
}
@ -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() {

53
runtime/yanic.go

@ -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)
nodes *Nodes
}
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) {

4
ssh/execute.go

@ -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()

4
ssh/run.go

@ -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()

3
webroot/.babelrc

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

6
webroot/.gitignore

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

16
webroot/css/_login.less

@ -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
webroot/css/map.css → webroot/css/_map.less

61
webroot/css/_menu.less

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

29
webroot/css/_notify.less

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

25
webroot/css/_status.less

@ -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);
}
}
}

62
webroot/css/console.css

@ -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;
}

BIN
webroot/css/images/layers-2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
webroot/css/images/layers.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

BIN
webroot/css/images/marker-icon-2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
webroot/css/images/marker-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

BIN
webroot/css/images/marker-shadow.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

624
webroot/css/leaflet.css

@ -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;
}

100
webroot/css/main.css → webroot/css/styles.less

@ -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;

94
webroot/gulpfile.babel.js

@ -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'
]);

36
webroot/index.html

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