├── .gitignore ├── README.md ├── alfred-monitoring-proxy ├── README.md ├── alfred-monitoring-proxy └── setup.py ├── contrib ├── alfred.md ├── alfred_legacy_provider │ ├── .gitignore │ ├── alfred_allow_setting_source_mac_via_unix_sock.patch │ ├── crawl.py │ ├── crawl_all.sh │ ├── get_macs.sh │ ├── list_old.sh │ └── run.sh ├── crawl.py ├── debug_webapp.sh ├── find_bad_mesh.py ├── geolocate.py └── get_current_ap.py ├── ffmap ├── __init__.py ├── config.py ├── db │ ├── gws.py │ ├── hoods.py │ ├── init_db.py │ ├── routers.py │ ├── stats.py │ └── users.py ├── gwtools.py ├── hoodtools.py ├── mapnik │ ├── csv │ │ └── .gitignore │ ├── dynmapnik.py │ ├── hoods_poly.xml │ ├── hoods_v2.xml │ ├── routers.xml │ ├── routers_local.xml │ ├── routers_v2.xml │ ├── run.sh │ ├── setup.py │ └── tilestache.cfg ├── maptools.py ├── misc.py ├── mysqlconfig.example.py ├── mysqltools.py ├── routertools.py ├── stattools.py ├── systemd │ ├── uwsgi-ffmap.service │ └── uwsgi-tiles.service ├── usertools.py └── web │ ├── __init__.py │ ├── api.py │ ├── application.py │ ├── filters.py │ ├── helpers.py │ ├── static │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-theme.min.css │ │ │ └── bootstrap.min.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ └── bootstrap.min.js │ ├── css │ │ ├── datatables │ │ │ └── dataTables.bootstrap.min.css │ │ └── style.css │ ├── img │ │ ├── favicon.ico │ │ ├── freifunk.svg │ │ ├── offline.png │ │ ├── online.png │ │ ├── router.svg │ │ ├── router_blue.svg │ │ ├── router_blue_white.svg │ │ ├── router_direct_green.svg │ │ ├── router_direct_red.svg │ │ ├── router_direct_yellow.svg │ │ ├── router_green.svg │ │ ├── router_green_v2.svg │ │ ├── router_green_v2_white.svg │ │ ├── router_green_white.svg │ │ ├── router_grey.svg │ │ ├── router_grey_white.svg │ │ ├── router_red.svg │ │ ├── router_red_v2.svg │ │ ├── router_red_v2_white.svg │ │ ├── router_red_white.svg │ │ ├── router_yellow.svg │ │ ├── router_yellow_white.svg │ │ └── unknown.png │ ├── js │ │ ├── datatables │ │ │ ├── dataTables.bootstrap.min.js │ │ │ └── jquery.dataTables.min.js │ │ ├── graph.js │ │ ├── graph │ │ │ ├── date.js │ │ │ ├── jquery.flot.byte.js │ │ │ ├── jquery.flot.downsample.js │ │ │ ├── jquery.flot.hiddengraphs.js │ │ │ ├── jquery.flot.js │ │ │ ├── jquery.flot.pie.js │ │ │ ├── jquery.flot.resize.js │ │ │ ├── jquery.flot.selection.js │ │ │ ├── jquery.flot.time.js │ │ │ ├── jquery.flot.tooltip.js │ │ │ └── upstream.txt │ │ ├── jquery │ │ │ └── jquery.min.js │ │ └── map.js │ └── leaflet │ │ ├── images │ │ ├── layers-2x.png │ │ ├── layers.png │ │ ├── marker-icon-2x.png │ │ ├── marker-icon.png │ │ └── marker-shadow.png │ │ ├── leaflet.css │ │ └── leaflet.js │ └── templates │ ├── apidoc.html │ ├── bootstrap.html │ ├── gws.html │ ├── index.html │ ├── login.html │ ├── map.html │ ├── register.html │ ├── resetpw.html │ ├── router.html │ ├── router_list.html │ ├── statistics.html │ ├── user.html │ ├── user_list.html │ └── v2routers.html ├── gwinfo ├── gwinfofirmware.sh └── sendgwinfo.sh ├── install.sh ├── restart.sh ├── scripts ├── calcglobalstats.py ├── copyusers.py ├── crontiles.sh ├── csv2users.py ├── defragtable.py ├── defragtables.py ├── deletestats.py ├── deleteunlinked.py ├── readpolyhoods.py ├── setupcron.sh └── users2csv.py ├── setup.py ├── start.sh └── stop.sh /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | __pycache__ 3 | .*.swp 4 | ffmap/mysqlconfig.py 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Git Repository Logic 2 | * Frequent updates are made to the **testing** branch, which is considered "dirty". Commits appearing here may be quickly written, untested, incomplete, etc. This is where the development happens. 3 | * In unspecified intervals, the piled-up changes in the testing branch are reviewed, ordered and squashed to a smaller set of tidy commits. Those are then pushed to the **master** branch. 4 | * The tidy-up is marked by an empty commit "Realign with master" in the testing branch. This is roughly equivalent to a *merge*, although for an actual merge the commits would remain unaltered. 5 | * Development happens in the testing branch. Thus, *testing* is more up-to-date, but *master* is better to understand. 6 | * The Monitoring web server uses the testing branch. 7 | 8 | 9 | ## Debian Dependencies 10 | ```bash 11 | apt-get install mysql-server python3-mysqldb python python3 python3-requests python3-lxml python3-pip python3-flask python3-dateutil python3-numpy python3-scipy python3-mapnik python3-pip uwsgi-plugin-python3 nginx 12 | pip3 install wheel pymongo pillow modestmaps simplejson werkzeug 13 | ``` 14 | 15 | ## When updating 16 | ```bash 17 | apt-get install mysql-server python3-mysqldb python3-mapnik 18 | apt-get uninstall mongodb python-mapnik uwsgi-plugin-python tilestache 19 | pip3 install wheel pillow modestmaps simplejson werkzeug 20 | pip3 uninstall uuid 21 | ``` 22 | 23 | ## Prerequisites 24 | * Datenbank in MySQL anlegen 25 | * Git vorbereiten: 26 | ```bash 27 | git clone https://github.com/asdil12/fff-monitoring 28 | git clone https://github.com/TileStache/TileStache 29 | cd fff-monitoring 30 | cp ffmap/mysqlconfig.example.py ffmap/mysqlconfig.py 31 | ``` 32 | * MySQL Zugangsdaten in mysqlconfig.py eintragen 33 | 34 | 35 | ## Installation 36 | ```bash 37 | ./install.sh 38 | systemctl daemon-reload 39 | systemctl enable uwsgi-ffmap 40 | systemctl enable uwsgi-tiles 41 | systemctl start uwsgi-ffmap 42 | systemctl start uwsgi-tiles 43 | cd ffmap/db/ 44 | ./init_db.py 45 | # Then apply NGINX Config 46 | cd ../.. # go back to fff-monitoring root directory 47 | ./scripts/setupcron.sh 48 | ``` 49 | 50 | ## NGINX Config 51 | ```nginx 52 | server { 53 | listen 443 ssl default_server; 54 | listen [::]:443 ssl default_server; 55 | 56 | ... 57 | 58 | location / { 59 | include uwsgi_params; 60 | uwsgi_pass 127.0.0.1:3031; 61 | client_max_body_size 30M; 62 | } 63 | 64 | location /tiles { 65 | include uwsgi_params; 66 | uwsgi_pass 127.0.0.1:3032; 67 | } 68 | 69 | location /static/ { 70 | root /usr/share/ffmap/; 71 | expires max; 72 | add_header Cache-Control "public"; 73 | } 74 | 75 | ... 76 | 77 | } 78 | ``` 79 | 80 | ## Admin anlegen 81 | * User über WebUI anlegen 82 | * Dann über z.B. phpmyadmin in der Tabelle users 'admin' auf 1 setzen 83 | -------------------------------------------------------------------------------- /alfred-monitoring-proxy/README.md: -------------------------------------------------------------------------------- 1 | Alfred <--> Monitoring Proxy 2 | ============================ 3 | 4 | Installation 5 | ------------ 6 | 7 | ``` 8 | pip3 install pyalfred 9 | 10 | python3 setup.py install 11 | ``` 12 | 13 | Then create a cron job to run `/usr/local/bin/alfred-monitoring-proxy` every 5 minutes. 14 | -------------------------------------------------------------------------------- /alfred-monitoring-proxy/alfred-monitoring-proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pyalfred 4 | import requests 5 | 6 | CONFIG = { 7 | "api_url": "https://monitoring.freifunk-franken.de/api/alfred", 8 | "fetch_ids": [64] 9 | } 10 | 11 | ac = pyalfred.AlfredConnection() 12 | 13 | for req_data_type in CONFIG["fetch_ids"]: 14 | data = {req_data_type: ac.fetch(req_data_type)} 15 | response = requests.post(CONFIG["api_url"], json=data) 16 | response.raise_for_status() 17 | for data_type, data in response.json().items(): 18 | ac.send(int(data_type), data) 19 | -------------------------------------------------------------------------------- /alfred-monitoring-proxy/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='alfred-monitoring-proxy', 7 | version='0.0.1', 8 | license='GPL', 9 | description='FFF ALFRED <--> Monitoring Proxy', 10 | author='Dominik Heidler', 11 | author_email='dominik@heidler.eu', 12 | url='http://github.com/asdil12/fff-monitoring', 13 | requires=['pyalfred'], 14 | scripts=['alfred-monitoring-proxy'], 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /contrib/alfred.md: -------------------------------------------------------------------------------- 1 | # Start Master Server 2 | ```bash 3 | alfred -i wlan0 -b none -m 4 | # -b batmanif to be used on router 5 | ``` 6 | 7 | # Save Data 8 | ```bash 9 | # Note that 0 - 63 are reserved (please send an e-mail to the 10 | # authors if you want to register a datatype), and can not be used 11 | # on the commandline. Information must be periodically written 12 | # again to alfred, otherwise it will timeout and alfred will for- 13 | # get about it (after 10 minutes). 14 | cat r.xml | gzip | alfred -s 64 15 | ``` 16 | 17 | # Load Data 18 | ```bash 19 | # 00:16:ea:c3:b8:26 is the mac of the sender 20 | alfred-json -z -f string -r 64 | python -c 'import sys,json;print(json.load(sys.stdin)["00:16:ea:c3:b8:26"])' 21 | ``` 22 | 23 | # Slave Config 24 | ``` 25 | config 'alfred' 'alfred' 26 | option interface 'br-mesh' 27 | option mode 'slave' 28 | option batmanif 'bat0' 29 | option start_vis '0' 30 | option run_facters '1' 31 | # REMOVE THIS LINE TO ENABLE ALFRED 32 | # option disabled '1' 33 | ``` 34 | 35 | 36 | ## Install ALFRED on the Router 37 | If the router has no IP, you will need to scp: 38 | ``` 39 | scp data.tar.gz root@[fe80::fad1:11ff:fe30:0abc%wlan0]:/tmp/ 40 | ``` 41 | 42 | ``` 43 | cd /tmp/ 44 | wget http://upload.kruton.de/files/1444228240/data.tar.gz 45 | cd / 46 | tar xzvf /tmp/data.tar.gz 47 | uci set alfred.alfred.interface=br-mesh 48 | uci set alfred.alfred.mode=slave 49 | uci set alfred.alfred.start_vis=0 50 | uci set alfred.alfred.run_facters=1 51 | uci set alfred.alfred.batmanif=bat0 52 | uci set alfred.alfred.disabled=0 53 | uci commit 54 | echo -e "#!/bin/sh\n\ncat /tmp/crawldata/node.data | alfred -s 64" > /etc/alfred/send_xml.sh 55 | chmod +x /etc/alfred/send_xml.sh 56 | /etc/init.d/alfred enable 57 | /etc/init.d/alfred start 58 | ``` 59 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/.gitignore: -------------------------------------------------------------------------------- 1 | macs.txt 2 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/alfred_allow_setting_source_mac_via_unix_sock.patch: -------------------------------------------------------------------------------- 1 | From: Dominik Heidler 2 | Date: Mon, 29 Feb 2016 19:07:00 +0100 3 | Subject: [PATCH] Allow setting the source mac via unix sock 4 | 5 | The server will only overwrite the mac if it is zero. 6 | The alfred client sets the mac to zero by default 7 | so this shouldn't break existing behaviour. 8 | --- 9 | unix_sock.c | 4 +++- 10 | 1 file changed, 3 insertions(+), 1 deletion(-) 11 | 12 | diff --git a/unix_sock.c b/unix_sock.c 13 | index 3c7e583..12a10e6 100644 14 | --- a/unix_sock.c 15 | +++ b/unix_sock.c 16 | @@ -119,7 +119,9 @@ static int unix_sock_add_data(struct globals *globals, 17 | 18 | data = push->data; 19 | data_len = ntohs(data->header.length); 20 | - memcpy(data->source, &interface->hwaddr, sizeof(interface->hwaddr)); 21 | + static const char zero[ETH_ALEN] = { 0 }; 22 | + if (!memcmp(zero, data->source, sizeof(data->source))) 23 | + memcpy(data->source, &interface->hwaddr, sizeof(interface->hwaddr)); 24 | 25 | if ((int)(data_len + sizeof(*data)) > len) 26 | goto err; 27 | -- 28 | 1.7.10.4 29 | 30 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/crawl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import subprocess 5 | import re 6 | import pyalfred 7 | 8 | 9 | 10 | CONFIG = { 11 | #"crawl_outgoing_netif": "br-mesh", 12 | # this old system sucks 13 | "crawl_outgoing_netif": "25%s" % open("/sys/class/net/br-mesh/ifindex").read().strip(), 14 | } 15 | 16 | def mac_to_ipv6_linklocal(mac): 17 | # Remove the most common delimiters; dots, dashes, etc. 18 | mac_bare = re.sub('[%s]+' % re.escape(' .:-'), '', mac) 19 | mac_value = int(mac_bare, 16) 20 | 21 | # Split out the bytes that slot into the IPv6 address 22 | # XOR the most significant byte with 0x02, inverting the 23 | # Universal / Local bit 24 | high2 = mac_value >> 32 & 0xffff ^ 0x0200 25 | high1 = mac_value >> 24 & 0xff 26 | low1 = mac_value >> 16 & 0xff 27 | low2 = mac_value & 0xffff 28 | 29 | return 'fe80::{:04x}:{:02x}ff:fe{:02x}:{:04x}'.format(high2, high1, low1, low2) 30 | 31 | mac = sys.argv[1] 32 | fe80_ip = mac_to_ipv6_linklocal(mac) 33 | 34 | node_data = subprocess.check_output(["curl", "-s", "--max-time", "5", "-g", "http://[%s%%%s]/node.data" % ( 35 | fe80_ip, 36 | CONFIG["crawl_outgoing_netif"] 37 | )]) 38 | try: 39 | node_data = gzip.decompress(node_data) 40 | except: 41 | pass 42 | 43 | assert "404" not in str(node_data).upper() 44 | 45 | 46 | #print(node_data) 47 | 48 | ac = pyalfred.AlfredConnection() 49 | ac.send(64, node_data.decode("UTF-8", errors="replace"), mac, gzip_data=True) 50 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/crawl_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for mac in `cat /home/dominik/alfred_legacy_provider/macs.txt` ; do 4 | echo "Crawling $mac" 5 | /home/dominik/alfred_legacy_provider/crawl.py $mac 6 | done 7 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/get_macs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /home/dominik/alfred_legacy_provider/list_old.sh 2>/dev/null | grep br-mesh | awk -F ' ' '{print $NF}' > /home/dominik/alfred_legacy_provider/macs.txt 4 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/list_old.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MYSQL_PWD="`grep mysql_password /var/www/netmon/config/config.local.inc.php | cut -d '"' -f 2`" 4 | mysql -u netmon -"p$MYSQL_PWD" netmon -e 'SELECT r.id, r.hostname, c.firmware_version, i.name, i.mac_addr FROM routers r JOIN crawl_routers c ON c.id = (SELECT MAX(id) FROM crawl_routers WHERE router_id = r.id) LEFT JOIN crawl_interfaces i ON i.id = (SELECT MAX(id) FROM crawl_interfaces WHERE router_id = r.id AND name = "br-mesh") WHERE c.status = "online" AND c.firmware_version <= "0.5.0" ORDER BY r.id;'; 5 | -------------------------------------------------------------------------------- /contrib/alfred_legacy_provider/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./get_macs.sh 4 | ./crawl_all.sh 5 | -------------------------------------------------------------------------------- /contrib/crawl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import lxml.etree 4 | import requests 5 | import time 6 | import subprocess 7 | import gzip 8 | import datetime 9 | from queue import Queue 10 | from threading import Thread 11 | from pymongo import MongoClient 12 | client = MongoClient() 13 | 14 | db = client.freifunk 15 | 16 | CONFIG = { 17 | "crawl_netif": "br-mesh", 18 | "mac_netif": "br-mesh", 19 | "vpn_netif": "fffVPN", 20 | "crawl_outgoing_netif": "wlan0", 21 | "num_crawler_threads": 10 22 | } 23 | 24 | crawl_hood = "nuernberg" 25 | 26 | def crawl(router): 27 | print("Crawling »%(hostname)s«" % router) 28 | crawl_ip = next(netif["ipv6_fe80_addr"] for netif in router["netifs"] if netif["name"] == CONFIG["crawl_netif"]) 29 | try: 30 | node_data = subprocess.check_output(["curl", "-s", "--max-time", "5", "http://[%s%%%s]/node.data" % ( 31 | crawl_ip, 32 | CONFIG["crawl_outgoing_netif"] 33 | )]) 34 | try: 35 | node_data = gzip.decompress(node_data) 36 | except: 37 | pass 38 | 39 | assert "<TITLE>404" not in str(node_data).upper() 40 | 41 | tree = lxml.etree.fromstring(node_data) 42 | print(" --> " + tree.xpath("/data/system_data/hostname/text()")[0]) 43 | 44 | router_update = { 45 | "status": tree.xpath("/data/system_data/status/text()")[0], 46 | "hostname": tree.xpath("/data/system_data/hostname/text()")[0], 47 | "neighbours": [], 48 | "netifs": [], 49 | "system": { 50 | "time": datetime.datetime.fromtimestamp(int(tree.xpath("/data/system_data/local_time/text()")[0])), 51 | "uptime": int(float(tree.xpath("/data/system_data/uptime/text()")[0])), 52 | "memory": { 53 | "free": int(tree.xpath("/data/system_data/memory_free/text()")[0]), 54 | "buffering": int(tree.xpath("/data/system_data/memory_buffering/text()")[0]), 55 | "caching": int(tree.xpath("/data/system_data/memory_caching/text()")[0]), 56 | }, 57 | "loadavg": float(tree.xpath("/data/system_data/loadavg/text()")[0]), 58 | "processes": { 59 | "runnable": int(tree.xpath("/data/system_data/processes/text()")[0].split("/")[0]), 60 | "total": int(tree.xpath("/data/system_data/processes/text()")[0].split("/")[1]), 61 | }, 62 | "clients": int(tree.xpath("/data/client_count/text()")[0]), 63 | "has_wan_uplink": len(tree.xpath("/data/interface_data/fffVPN")) > 0, 64 | }, 65 | "hardware": { 66 | "chipset": tree.xpath("/data/system_data/chipset/text()")[0], 67 | "cpu": tree.xpath("/data/system_data/cpu/text()")[0] 68 | }, 69 | "software": { 70 | "os": "%s (%s)" % (tree.xpath("/data/system_data/distname/text()")[0], 71 | tree.xpath("/data/system_data/distversion/text()")[0]), 72 | "batman_adv": tree.xpath("/data/system_data/batman_advanced_version/text()")[0], 73 | "kernel": tree.xpath("/data/system_data/kernel_version/text()")[0], 74 | "nodewatcher": tree.xpath("/data/system_data/nodewatcher_version/text()")[0], 75 | #"fastd": tree.xpath("/data/system_data/fastd_version/text()")[0], 76 | "firmware": tree.xpath("/data/system_data/firmware_version/text()")[0], 77 | "firmware_rev": tree.xpath("/data/system_data/firmware_revision/text()")[0], 78 | } 79 | } 80 | 81 | # get hardware.name by chipset 82 | chipset = db.chipsets.find_one({"name": router_update["hardware"]["chipset"]}) 83 | if chipset: 84 | router_update["hardware"]["name"] = chipset["hardware"] 85 | else: 86 | print("Unknown Chipset: %s" % router_update["hardware"]["chipset"]) 87 | 88 | for netif in tree.xpath("/data/interface_data/*"): 89 | interface = { 90 | "name": netif.xpath("name/text()")[0], 91 | "mtu": int(netif.xpath("mtu/text()")[0]), 92 | "mac": netif.xpath("mac_addr/text()")[0].lower(), 93 | "traffic": { 94 | "rx": int(netif.xpath("traffic_rx/text()")[0]), 95 | "tx": int(netif.xpath("traffic_tx/text()")[0]), 96 | }, 97 | } 98 | if len(netif.xpath("ipv6_link_local_addr/text()")) > 0: 99 | interface["ipv6_fe80_addr"] = netif.xpath("ipv6_link_local_addr/text()")[0].lower().split("/")[0] 100 | if len(netif.xpath("ipv4_addr/text()")) > 0: 101 | interface["ipv4_addr"] = netif.xpath("ipv4_addr/text()")[0] 102 | router_update["netifs"].append(interface) 103 | 104 | visible_neighbours = 0 105 | 106 | for originator in tree.xpath("/data/batman_adv_originators/*"): 107 | visible_neighbours += 1 108 | o_mac = originator.xpath("originator/text()")[0] 109 | o_nexthop = originator.xpath("nexthop/text()")[0] 110 | # mac is the mac of the neighbour w2/5mesh if 111 | # (which might also be called wlan0-1) 112 | o_link_quality = originator.xpath("link_quality/text()")[0] 113 | o_out_if = originator.xpath("outgoing_interface/text()")[0] 114 | if o_mac.upper() == o_nexthop.upper(): 115 | # skip vpn server 116 | if o_out_if == CONFIG["vpn_netif"]: 117 | continue 118 | neighbour = { 119 | "mac": o_mac.lower(), 120 | "netif": o_out_if, 121 | "quality": int(o_link_quality), 122 | } 123 | try: 124 | neighbour_router = db.routers.find_one({"netifs.mac": neighbour["mac"]}) 125 | neighbour["_id"] = neighbour_router["_id"] 126 | neighbour["hostname"] = neighbour_router["hostname"] 127 | assert "coordinates" in neighbour_router["position"] 128 | assert neighbour_router["position"]["coordinates"][0] != 0 129 | assert neighbour_router["position"]["coordinates"][1] != 0 130 | if "comment" in neighbour_router["position"]: 131 | del neighbour_router["position"]["comment"] 132 | neighbour["position"] = neighbour_router["position"] 133 | except: 134 | pass 135 | router_update["neighbours"].append(neighbour) 136 | 137 | router_update["system"]["visible_neighbours"] = visible_neighbours 138 | 139 | db.routers.update_one({"_id": router["_id"]}, {"$set": router_update, "$currentDate": {"last_contact": True}}) 140 | status = router_update["status"] 141 | except subprocess.CalledProcessError: 142 | # in a non-crawling setup the system would need to 143 | # mark routers as offline when the last_contact is too far in the past 144 | # eg by a cronjob 145 | db.routers.update_one({"_id": router["_id"]}, {"$set": {"status": "offline"}}) 146 | status = "offline" 147 | print(" --> OFFLINE") 148 | except (AssertionError, lxml.etree.XMLSyntaxError): 149 | db.routers.update_one({"_id": router["_id"]}, {"$set": {"status": "unknown"}}) 150 | status = "unknown" 151 | print(" --> UNKNOWN") 152 | finally: 153 | # fire events 154 | events = [] 155 | try: 156 | if router["system"]["uptime"] > router_update["system"]["uptime"]: 157 | events.append({ 158 | "time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc), 159 | "type": "reboot", 160 | }) 161 | except: 162 | pass 163 | if router["status"] != status: 164 | events.append({ 165 | "time": datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc), 166 | "type": status, 167 | }) 168 | db.routers.update_one({"_id": router["_id"]}, {"$push": {"events": { 169 | "$each": events, 170 | "$slice": -10, 171 | }}}) 172 | 173 | if status == "online": 174 | # calculate RRD statistics 175 | #FIXME: implementation 176 | pass 177 | 178 | 179 | q = Queue() 180 | keep_working = True 181 | 182 | def worker(): 183 | while keep_working: 184 | router = q.get() 185 | crawl(router) 186 | q.task_done() 187 | 188 | for i in range(CONFIG["num_crawler_threads"]): 189 | t = Thread(target=worker) 190 | t.daemon = True 191 | t.start() 192 | 193 | for router in db.routers.find({"netifs.name": CONFIG["crawl_netif"], "hood": crawl_hood}): 194 | q.put(router) 195 | 196 | # block until queue is empty 197 | q.join() 198 | 199 | # stop workers 200 | keep_working = False 201 | -------------------------------------------------------------------------------- /contrib/debug_webapp.sh: -------------------------------------------------------------------------------- 1 | uwsgi_python3 -w ffmap.web.application:app --http-socket :9090 --catch-exceptions 2 | -------------------------------------------------------------------------------- /contrib/find_bad_mesh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from pymongo import MongoClient 4 | client = MongoClient() 5 | 6 | db = client.freifunk 7 | 8 | for router in db.routers.find({"hood": {"$exists": True}, "neighbours": {"$exists": True}, "status": "online"}, {"stats": 0}): 9 | for neighbour in router["neighbours"]: 10 | if "_id" in neighbour and "position" in neighbour: 11 | neighbour_router = db.routers.find_one({"_id": neighbour["_id"]}, {"stats": 0}) 12 | if router["hood"] != neighbour_router["hood"] and neighbour_router["status"] == "online": 13 | print("Illegal inter-hood-mesh between %s (%s) and %s (%s)!" % (router["hostname"], router["hood"], neighbour_router["hostname"], neighbour_router["hood"])) 14 | -------------------------------------------------------------------------------- /contrib/geolocate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import requests 4 | import subprocess 5 | 6 | # doku: https://developers.google.com/maps/documentation/geolocation/intro#wifi_access_point_object 7 | 8 | """ 9 | r = requests.post("https://www.googleapis.com/geolocation/v1/geolocate", params={"key": "AIzaSyDwr302FpOSkGRpLlUpPThNTDPbXcIn_FM"}, json={ 10 | "wifiAccessPoints": [ 11 | { 12 | "macAddress": "10-fe-ed-af-43-44", 13 | "signalStrength": 100 14 | }, 15 | { 16 | "macAddress": "02-ca-ff-ee-ba-be", 17 | "signalStrength": 100 18 | } 19 | ] 20 | }) 21 | print(r.text) 22 | """ 23 | 24 | networks = [] 25 | 26 | o = subprocess.check_output(["iwlist", "wlan0", "scanning"]) 27 | ls = o.decode("UTF-8").split(" Cell") 28 | for wifi in ls[1:]: 29 | for field in wifi.split("\n"): 30 | if "Address:" in field: 31 | mac = field.split("Address: ")[1] 32 | elif "Signal level=" in field: 33 | signal_strength = field.split("Signal level=")[1].split(" dBm")[0] 34 | print("%s -> %s" % (mac, signal_strength)) 35 | networks.append({"macAddress": mac, "signalStrength": signal_strength}) 36 | 37 | r = requests.post("https://www.googleapis.com/geolocation/v1/geolocate", 38 | params={"key": "AIzaSyDwr302FpOSkGRpLlUpPThNTDPbXcIn_FM"}, 39 | json={"wifiAccessPoints": networks} 40 | ) 41 | 42 | print({"wifiAccessPoints": networks}) 43 | print(r.text) 44 | -------------------------------------------------------------------------------- /contrib/get_current_ap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import subprocess 4 | from pymongo import MongoClient 5 | client = MongoClient() 6 | 7 | db = client.freifunk 8 | 9 | # this tool will try to show you the hostname of the Freifunk AP you are currently connected to 10 | 11 | mac = subprocess.check_output(["iwgetid", "-ar"]).strip().lower().decode() 12 | 13 | router = db.routers.find_one({"netifs.mac": mac}) 14 | 15 | print(router["hostname"]) 16 | -------------------------------------------------------------------------------- /ffmap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/__init__.py -------------------------------------------------------------------------------- /ffmap/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | CONFIG = { 4 | "vpn_netif": "fffVPN", # Name of VPN interface 5 | "vpn_netif_l2tp": "l2tp", # Beginning of names of L2TP interfaces 6 | "vpn_netif_aux": "fffauxVPN", # Name of AUX interface 7 | "offline_threshold_minutes": 15, # Router switches to offline after X minutes 8 | "orphan_threshold_days": 10, # Router switches to orphaned state after X days 9 | "delete_threshold_days": 180, # Router is deleted after X days 10 | "gw_netif_threshold_hours": 48, # Hours which outdated netif from gwinfo is preserved for statistics 11 | "router_stat_days": 30, # Router stats are collected for X days 12 | "router_stat_netif": 10, # Router stats for netifs are collected for X days 13 | "router_stat_gw": 1, # Router stats for gw are collected for X days 14 | "router_stat_mindiff_secs": 10, # Time difference (uptime) in seconds required for a new entry in router stats 15 | "router_stat_mindiff_default": 270, # Time difference (router stats tables) in seconds required for a new entry in router stats 16 | "router_stat_mindiff_netif": 270, # Time difference (router netif stats) in seconds required for a new entry in router stats 17 | "event_num_entries": 300, # Number of events stored per router 18 | "global_stat_days": 365, # Global/hood stats are collected for X days 19 | "csv_dir": "/var/lib/ffmap/csv", # Directory where the .csv files for TileStache/mapnik are stored 20 | "debug_dir": "/data/fff/fffmonlog", # Output directory for debug .txt files 21 | } 22 | -------------------------------------------------------------------------------- /ffmap/db/gws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '../..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | mysql = FreifunkMySQL() 10 | 11 | mysql.execute(""" 12 | CREATE TABLE `gw` ( 13 | `id` smallint(5) UNSIGNED NOT NULL, 14 | `name` varchar(50) COLLATE utf8_unicode_ci NOT NULL, 15 | `stats_page` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL, 16 | `version` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL, 17 | `last_contact` datetime NOT NULL 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 19 | """) 20 | 21 | mysql.execute(""" 22 | ALTER TABLE `gw` 23 | ADD PRIMARY KEY (`id`), 24 | ADD UNIQUE KEY `name` (`name`) 25 | """) 26 | 27 | mysql.execute(""" 28 | ALTER TABLE `gw` 29 | MODIFY `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT 30 | """) 31 | 32 | mysql.execute(""" 33 | CREATE TABLE `gw_admin` ( 34 | `gw` smallint(5) UNSIGNED NOT NULL, 35 | `name` varchar(100) COLLATE utf8_unicode_ci NOT NULL, 36 | `prio` tinyint(3) UNSIGNED NOT NULL 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 38 | """) 39 | 40 | mysql.execute(""" 41 | ALTER TABLE `gw_admin` 42 | ADD PRIMARY KEY (`gw`,`name`) 43 | """) 44 | 45 | mysql.execute(""" 46 | CREATE TABLE `gw_netif` ( 47 | `gw` smallint(5) UNSIGNED NOT NULL, 48 | `mac` bigint(20) UNSIGNED NOT NULL, 49 | `netif` varchar(15) COLLATE utf8_unicode_ci NOT NULL, 50 | `vpnmac` bigint(20) UNSIGNED DEFAULT NULL, 51 | `ipv4` char(18) COLLATE utf8_unicode_ci DEFAULT NULL, 52 | `ipv6` varchar(60) COLLATE utf8_unicode_ci DEFAULT NULL, 53 | `dhcpstart` char(15) COLLATE utf8_unicode_ci DEFAULT NULL, 54 | `dhcpend` char(15) COLLATE utf8_unicode_ci DEFAULT NULL, 55 | `last_contact` datetime NOT NULL 56 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 57 | """) 58 | 59 | mysql.execute(""" 60 | ALTER TABLE `gw_netif` 61 | ADD PRIMARY KEY (`mac`), 62 | ADD KEY `gw` (`gw`) 63 | """) 64 | 65 | mysql.commit() 66 | 67 | mysql.close() 68 | -------------------------------------------------------------------------------- /ffmap/db/hoods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '../..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | mysql = FreifunkMySQL() 10 | 11 | mysql.execute(""" 12 | CREATE TABLE `hoods` ( 13 | `id` smallint(6) UNSIGNED NOT NULL, 14 | `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 16 | """) 17 | 18 | mysql.execute(""" 19 | ALTER TABLE `hoods` 20 | ADD PRIMARY KEY (`id`), 21 | ADD UNIQUE KEY `name` (`name`) 22 | """) 23 | 24 | mysql.execute(""" 25 | ALTER TABLE `hoods` 26 | MODIFY `id` smallint(6) UNSIGNED NOT NULL AUTO_INCREMENT 27 | """) 28 | 29 | mysql.execute(""" 30 | ALTER TABLE hoods AUTO_INCREMENT = 30001 31 | """) 32 | 33 | mysql.execute(""" 34 | INSERT INTO hoods (id, name) 35 | VALUES (%s, %s) 36 | """,(10100,Legacy,)) 37 | 38 | mysql.execute(""" 39 | CREATE TABLE `hoodsv2` ( 40 | `id` int(10) UNSIGNED NOT NULL, 41 | `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 42 | `net` varchar(30) COLLATE utf8_unicode_ci NOT NULL, 43 | `lat` double DEFAULT NULL, 44 | `lng` double DEFAULT NULL 45 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 46 | """) 47 | 48 | mysql.execute(""" 49 | ALTER TABLE `hoodsv2` 50 | ADD PRIMARY KEY (`id`), 51 | ADD UNIQUE KEY `name` (`name`), 52 | ADD KEY `lat` (`lat`), 53 | ADD KEY `lng` (`lng`) 54 | """) 55 | 56 | mysql.execute(""" 57 | CREATE TABLE `polygons` ( 58 | `id` int(10) UNSIGNED NOT NULL, 59 | `polyid` int(10) UNSIGNED NOT NULL, 60 | `lat` double NOT NULL, 61 | `lon` double NOT NULL 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 63 | """) 64 | 65 | mysql.execute(""" 66 | ALTER TABLE `polygons` 67 | ADD PRIMARY KEY (`id`), 68 | ADD KEY `polyid` (`polyid`) 69 | """) 70 | 71 | mysql.execute(""" 72 | ALTER TABLE `polygons` 73 | MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT 74 | """) 75 | 76 | mysql.execute(""" 77 | CREATE TABLE `polyhoods` ( 78 | `polyid` int(10) UNSIGNED NOT NULL, 79 | `hoodid` int(10) UNSIGNED NOT NULL 80 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 81 | """) 82 | 83 | mysql.execute(""" 84 | ALTER TABLE `polyhoods` 85 | ADD PRIMARY KEY (`polyid`) 86 | """) 87 | 88 | mysql.execute(""" 89 | ALTER TABLE `polyhoods` 90 | MODIFY `polyid` int(10) UNSIGNED NOT NULL AUTO_INCREMENT 91 | """) 92 | 93 | mysql.commit() 94 | 95 | mysql.close() 96 | -------------------------------------------------------------------------------- /ffmap/db/init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import routers 4 | import hoods 5 | import stats 6 | import gws 7 | import users 8 | -------------------------------------------------------------------------------- /ffmap/db/stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '../..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | mysql = FreifunkMySQL() 10 | 11 | mysql.execute(""" 12 | CREATE TABLE `stats_global` ( 13 | `time` int(11) NOT NULL, 14 | `clients` mediumint(9) NOT NULL, 15 | `online` smallint(6) NOT NULL, 16 | `offline` smallint(6) NOT NULL, 17 | `unknown` smallint(6) NOT NULL, 18 | `orphaned` smallint(6) NOT NULL, 19 | `rx` int(10) UNSIGNED DEFAULT NULL, 20 | `tx` int(10) UNSIGNED DEFAULT NULL 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 22 | """) 23 | 24 | mysql.execute(""" 25 | ALTER TABLE `stats_global` 26 | ADD PRIMARY KEY (`time`) 27 | """) 28 | 29 | mysql.execute(""" 30 | CREATE TABLE `stats_gw` ( 31 | `time` int(11) NOT NULL, 32 | `mac` bigint(20) UNSIGNED NOT NULL, 33 | `clients` mediumint(9) NOT NULL, 34 | `online` smallint(6) NOT NULL, 35 | `offline` smallint(6) NOT NULL, 36 | `unknown` smallint(6) NOT NULL, 37 | `orphaned` smallint(6) NOT NULL 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 39 | """) 40 | 41 | mysql.execute(""" 42 | ALTER TABLE `stats_gw` 43 | ADD PRIMARY KEY (`time`,`mac`), 44 | ADD KEY `mac` (`mac`) 45 | """) 46 | 47 | mysql.execute(""" 48 | CREATE TABLE `stats_hood` ( 49 | `time` int(11) NOT NULL, 50 | `hood` smallint(5) UNSIGNED NOT NULL, 51 | `clients` mediumint(9) NOT NULL, 52 | `online` smallint(6) NOT NULL, 53 | `offline` smallint(6) NOT NULL, 54 | `unknown` smallint(6) NOT NULL, 55 | `orphaned` smallint(6) NOT NULL, 56 | `rx` int(10) UNSIGNED DEFAULT NULL, 57 | `tx` int(10) UNSIGNED DEFAULT NULL 58 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 59 | """) 60 | 61 | mysql.execute(""" 62 | ALTER TABLE `stats_hood` 63 | ADD PRIMARY KEY (`time`,`hood`), 64 | ADD KEY `hood` (`hood`) 65 | """) 66 | 67 | mysql.commit() 68 | 69 | mysql.close() 70 | -------------------------------------------------------------------------------- /ffmap/db/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '../..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | mysql = FreifunkMySQL() 10 | 11 | mysql.execute(""" 12 | CREATE TABLE `users` ( 13 | `id` int(11) NOT NULL, 14 | `nickname` varchar(200) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 15 | `password` varchar(250) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 16 | `token` varchar(250) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 17 | `email` varchar(200) COLLATE utf8_unicode_ci NOT NULL, 18 | `created` datetime NOT NULL, 19 | `admin` tinyint(1) NOT NULL DEFAULT '0', 20 | `abuse` tinyint(1) NOT NULL DEFAULT '0' 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 22 | """) 23 | 24 | mysql.execute(""" 25 | ALTER TABLE `users` 26 | ADD PRIMARY KEY (`id`), 27 | ADD UNIQUE KEY `nickname` (`nickname`), 28 | ADD UNIQUE KEY `email` (`email`) 29 | """) 30 | 31 | mysql.execute(""" 32 | ALTER TABLE `users` 33 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT 34 | """) 35 | 36 | mysql.commit() 37 | 38 | mysql.close() 39 | -------------------------------------------------------------------------------- /ffmap/gwtools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | from ffmap.misc import * 9 | from ffmap.config import CONFIG 10 | from flask import request, url_for 11 | 12 | import datetime 13 | import time 14 | 15 | def import_gw_data(mysql, gw_data): 16 | if "hostname" in gw_data and "netifs" in gw_data: 17 | time = utcnow().strftime('%Y-%m-%d %H:%M:%S') 18 | stats_page = gw_data.get("stats_page","") 19 | version = gw_data.get("version","") 20 | 21 | # Make None if empty (gw_data.get() only checks for existing key) 22 | if not stats_page: 23 | stats_page = None 24 | if not version: 25 | version = None 26 | newid = mysql.findone("SELECT id FROM gw WHERE name = %s LIMIT 1",(gw_data["hostname"],),"id") 27 | if newid: 28 | mysql.execute(""" 29 | UPDATE gw 30 | SET stats_page = %s, version = %s, last_contact = %s 31 | WHERE id = %s 32 | """,(stats_page,version,time,newid,)) 33 | mysql.execute(""" 34 | UPDATE gw_netif 35 | SET ipv4 = NULL, ipv6 = NULL, dhcpstart = NULL, dhcpend = NULL 36 | WHERE gw = %s 37 | """,(newid,)) 38 | else: 39 | mysql.execute(""" 40 | INSERT INTO gw (name, stats_page, version, last_contact) 41 | VALUES (%s, %s, %s, %s) 42 | """,(gw_data["hostname"],stats_page,version,time,)) 43 | newid = mysql.cursor().lastrowid 44 | 45 | nmacs = {} 46 | for n in gw_data["netifs"]: 47 | nmacs[n["netif"]] = n["mac"] 48 | 49 | ndata = [] 50 | for n in gw_data["netifs"]: 51 | if len(n["mac"])<17 or len(n["mac"])>17: 52 | continue 53 | if n["netif"].startswith("l2tp"): # Filter l2tp interfaces 54 | continue 55 | if "vpnif" in n and n["vpnif"]: 56 | n["vpnmac"] = nmacs.get(n["vpnif"],None) 57 | else: 58 | n["vpnmac"] = None 59 | if not "ipv4" in n or not n["ipv4"]: 60 | n["ipv4"] = None 61 | if not "ipv6" in n or not n["ipv6"]: 62 | n["ipv6"] = None 63 | if not "dhcpstart" in n or not n["dhcpstart"]: 64 | n["dhcpstart"] = None 65 | if not "dhcpend" in n or not n["dhcpend"]: 66 | n["dhcpend"] = None 67 | 68 | ndata.append((newid,mac2int(n["mac"]),n["netif"],mac2int(n["vpnmac"]),n["ipv4"],n["ipv6"],n["dhcpstart"],n["dhcpend"],time,)) 69 | 70 | mysql.executemany(""" 71 | INSERT INTO gw_netif (gw, mac, netif, vpnmac, ipv4, ipv6, dhcpstart, dhcpend, last_contact) 72 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) 73 | ON DUPLICATE KEY UPDATE 74 | gw=VALUES(gw), 75 | netif=VALUES(netif), 76 | vpnmac=VALUES(vpnmac), 77 | ipv4=VALUES(ipv4), 78 | ipv6=VALUES(ipv6), 79 | dhcpstart=VALUES(dhcpstart), 80 | dhcpend=VALUES(dhcpend), 81 | last_contact=VALUES(last_contact) 82 | """,ndata) 83 | 84 | adata = [] 85 | aid = 0 86 | for a in gw_data["admins"]: 87 | aid += 1 88 | adata.append((newid,a,aid,)) 89 | 90 | mysql.execute("DELETE FROM gw_admin WHERE gw = %s",(newid,)) 91 | mysql.executemany(""" 92 | INSERT INTO gw_admin (gw, name, prio) 93 | VALUES (%s, %s, %s) 94 | ON DUPLICATE KEY UPDATE 95 | prio=VALUES(prio) 96 | """,adata) 97 | else: 98 | writelog(CONFIG["debug_dir"] + "/fail_gwinfo.txt", "{} - Corrupted file.".format(request.environ['REMOTE_ADDR'])) 99 | 100 | def gw_name(gw): 101 | if gw["gw"] and gw["gwif"]: 102 | s = gw["gw"] + " (" + gw["gwif"] + ")" 103 | else: 104 | s = int2mac(gw["mac"]) 105 | return s 106 | 107 | def gw_bat(gw): 108 | if gw["batif"] and gw["batmac"]: 109 | s = int2mac(gw["batmac"]) + " (" + gw["batif"] + ")" 110 | else: 111 | s = "---" 112 | return s 113 | 114 | def delete_unlinked_gws(mysql): 115 | # Delete entries in gw_* tables without corresponding gw in master table 116 | 117 | tables = ["gw_admin","gw_netif"] 118 | 119 | for t in tables: 120 | start_time = time.time() 121 | mysql.execute(""" 122 | DELETE d FROM {} AS d 123 | LEFT JOIN gw AS g ON g.id = d.gw 124 | WHERE g.id IS NULL 125 | """.format(t)) 126 | print("--- Deleted %i rows from %s: %.3f seconds ---" % (mysql.cursor().rowcount,t,time.time() - start_time)) 127 | mysql.commit() 128 | 129 | -------------------------------------------------------------------------------- /ffmap/hoodtools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | import urllib.request, urllib.error, json 10 | import math 11 | 12 | def update_hoods_v2(mysql): 13 | try: 14 | with urllib.request.urlopen("http://keyserver.freifunk-franken.de/v2/hoods.php") as url: 15 | hoodskx = json.loads(url.read().decode()) 16 | 17 | kx_keys = [] 18 | kx_data = [] 19 | for kx in hoodskx: 20 | kx_keys.append(kx["id"]) 21 | kx_data.append((kx["id"],kx["name"],kx["net"],kx.get("lat",None),kx.get("lon",None),)) 22 | 23 | # Delete entries in DB where hood is missing in KeyXchange 24 | db_keys = mysql.fetchall("SELECT id FROM hoodsv2",(),"id") 25 | for n in db_keys: 26 | if n in kx_keys: 27 | continue 28 | mysql.execute("DELETE FROM hoodsv2 WHERE id = %s",(n,)) 29 | 30 | # Create/update entries from KeyXchange to DB 31 | mysql.executemany(""" 32 | INSERT INTO hoodsv2 (id, name, net, lat, lng) 33 | VALUES (%s, %s, %s, %s, %s) 34 | ON DUPLICATE KEY UPDATE 35 | name=VALUES(name), 36 | net=VALUES(net), 37 | lat=VALUES(lat), 38 | lng=VALUES(lng) 39 | """,kx_data) 40 | 41 | except urllib.error.HTTPError as e: 42 | return 43 | 44 | def update_hoods_poly(mysql): 45 | try: 46 | #with urllib.request.urlopen("http://keyserver.freifunk-franken.de/v2/hoods.php") as url: 47 | with urllib.request.urlopen("https://lauch.org/keyxchange/hoods.php") as url: 48 | hoodskx = json.loads(url.read().decode()) 49 | 50 | mysql.execute("DELETE FROM polygons",()) 51 | mysql.execute("DELETE FROM polyhoods",()) 52 | 53 | for kx in hoodskx: 54 | for polygon in kx.get("polygons",()): 55 | mysql.execute(""" 56 | INSERT INTO polyhoods (hoodid) 57 | VALUES (%s) 58 | """,(kx["id"],)) 59 | newid = mysql.cursor().lastrowid 60 | vertices = [] 61 | for p in polygon: 62 | vertices.append((newid,p["lat"],p["lon"],)) 63 | mysql.executemany(""" 64 | INSERT INTO polygons (polyid, lat, lon) 65 | VALUES (%s, %s, %s) 66 | """,vertices) 67 | 68 | except urllib.error.HTTPError as e: 69 | return 70 | -------------------------------------------------------------------------------- /ffmap/mapnik/csv/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | -------------------------------------------------------------------------------- /ffmap/mapnik/dynmapnik.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | sys.path.insert(0,'/data/fff/TileStache') 5 | 6 | import os 7 | import logging 8 | import TileStache 9 | 10 | class DynMapnik(TileStache.Providers.Mapnik): 11 | def __init__(self, *args, **kwargs): 12 | self.mapfile_mtime = 0 13 | TileStache.Providers.Mapnik.__init__(self, *args, **kwargs) 14 | def renderArea(self, *args, **kwargs): 15 | cur_mapfile_mtime = os.path.getmtime(self.mapfile) 16 | if cur_mapfile_mtime > self.mapfile_mtime: 17 | self.mapfile_mtime = cur_mapfile_mtime 18 | if self.mapnik is not None: 19 | self.mapnik = None 20 | logging.info('TileStache.DynMapnik.ImageProvider.renderArea() detected mapfile change') 21 | return TileStache.Providers.Mapnik.renderArea(self, *args, **kwargs) 22 | -------------------------------------------------------------------------------- /ffmap/mapnik/hoods_poly.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!DOCTYPE Map> 3 | <Map background-color="transparent" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"> 4 | <Style name="hoodpoint"> 5 | <Rule> 6 | <TextSymbolizer face-name="DejaVu Sans Book" size="12" fill="#b200b2" halo-radius="2">[name]</TextSymbolizer> 7 | </Rule> 8 | </Style> 9 | <Style name="hoodborder"> 10 | <Rule> 11 | <LineSymbolizer stroke-width="3" stroke="#b200b2" stroke-linecap="butt" stroke-dasharray="6, 2" clip="false" /> 12 | </Rule> 13 | </Style> 14 | <Layer name="borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 15 | <StyleName>hoodborder</StyleName> 16 | <Datasource> 17 | <Parameter name="type">csv</Parameter> 18 | <Parameter name="file">csv/hoods_poly.csv</Parameter> 19 | </Datasource> 20 | </Layer> 21 | <Layer name="points" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 22 | <StyleName>hoodpoint</StyleName> 23 | <Datasource> 24 | <Parameter name="type">csv</Parameter> 25 | <Parameter name="file">csv/hood-points-poly.csv</Parameter> 26 | </Datasource> 27 | </Layer> 28 | </Map> 29 | -------------------------------------------------------------------------------- /ffmap/mapnik/hoods_v2.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!DOCTYPE Map> 3 | <Map background-color="transparent" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"> 4 | <Style name="hoodpoint"> 5 | <Rule> 6 | <TextSymbolizer face-name="DejaVu Sans Book" size="12" fill="#1e42ff" halo-radius="2">[name]</TextSymbolizer> 7 | </Rule> 8 | </Style> 9 | <Style name="hoodborder"> 10 | <Rule> 11 | <LineSymbolizer stroke-width="3" stroke="#1e42ff" stroke-linecap="butt" stroke-dasharray="6, 2" clip="false" /> 12 | </Rule> 13 | </Style> 14 | <Layer name="borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 15 | <StyleName>hoodborder</StyleName> 16 | <Datasource> 17 | <Parameter name="type">csv</Parameter> 18 | <Parameter name="file">csv/hoods_v2.csv</Parameter> 19 | </Datasource> 20 | </Layer> 21 | <Layer name="points" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 22 | <StyleName>hoodpoint</StyleName> 23 | <Datasource> 24 | <Parameter name="type">csv</Parameter> 25 | <Parameter name="file">csv/hood-points-v2.csv</Parameter> 26 | </Datasource> 27 | </Layer> 28 | </Map> 29 | -------------------------------------------------------------------------------- /ffmap/mapnik/routers.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!DOCTYPE Map> 3 | <Map background-color="transparent" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"> 4 | <Style name="routerpoint" filter-mode="first"> 5 | <Rule> 6 | <Filter>([status] = 'online')</Filter> 7 | <!-- For directed antenna 8 | <PointSymbolizer file="static/img/router_direct_green.svg" allow-overlap="true" transform="rotate(45)" /> 9 | --> 10 | <PointSymbolizer file="static/img/router_green.svg" allow-overlap="true" /> 11 | </Rule> 12 | <Rule> 13 | <Filter>([status] = 'offline')</Filter> 14 | <PointSymbolizer file="static/img/router_red.svg" allow-overlap="true" /> 15 | </Rule> 16 | <Rule> 17 | <Filter>([status] = 'unknown')</Filter> 18 | <PointSymbolizer file="static/img/router_yellow.svg" allow-overlap="true" /> 19 | </Rule> 20 | <Rule> 21 | <Filter>([status] = 'orphaned')</Filter> 22 | <PointSymbolizer file="static/img/router_grey.svg" allow-overlap="true" /> 23 | </Rule> 24 | <Rule> 25 | <Filter>([status] = 'online_wan')</Filter> 26 | <PointSymbolizer file="static/img/router_green_white.svg" allow-overlap="true" /> 27 | </Rule> 28 | <Rule> 29 | <Filter>([status] = 'offline_wan')</Filter> 30 | <PointSymbolizer file="static/img/router_red_white.svg" allow-overlap="true" /> 31 | </Rule> 32 | <Rule> 33 | <Filter>([status] = 'unknown_wan')</Filter> 34 | <PointSymbolizer file="static/img/router_yellow_white.svg" allow-overlap="true" /> 35 | </Rule> 36 | <Rule> 37 | <Filter>([status] = 'orphaned_wan')</Filter> 38 | <PointSymbolizer file="static/img/router_grey_white.svg" allow-overlap="true" /> 39 | </Rule> 40 | </Style> 41 | <Style name="color" filter-mode="first"> 42 | <Rule> 43 | <Filter>([quality] < 1)</Filter> 44 | <LineSymbolizer stroke-width="3" stroke="#008c00" stroke-linecap="butt" clip="false" /> 45 | </Rule> 46 | <Rule> 47 | <Filter>([quality] < 105)</Filter> 48 | <LineSymbolizer stroke-width="3" stroke="#ff1e1e" stroke-linecap="butt" clip="false" /> 49 | </Rule> 50 | <Rule> 51 | <Filter>([quality] < 130)</Filter> 52 | <LineSymbolizer stroke-width="3" stroke="#ff4949" stroke-linecap="butt" clip="false" /> 53 | </Rule> 54 | <Rule> 55 | <Filter>([quality] < 155)</Filter> 56 | <LineSymbolizer stroke-width="3" stroke="#ff6a6a" stroke-linecap="butt" clip="false" /> 57 | </Rule> 58 | <Rule> 59 | <Filter>([quality] < 180)</Filter> 60 | <LineSymbolizer stroke-width="3" stroke="#ffac53" stroke-linecap="butt" clip="false" /> 61 | </Rule> 62 | <Rule> 63 | <Filter>([quality] < 205)</Filter> 64 | <LineSymbolizer stroke-width="3" stroke="#ffeb79" stroke-linecap="butt" clip="false" /> 65 | </Rule> 66 | <Rule> 67 | <Filter>([quality] < 230)</Filter> 68 | <LineSymbolizer stroke-width="3" stroke="#79ff7c" stroke-linecap="butt" clip="false" /> 69 | </Rule> 70 | <Rule> 71 | <Filter>([quality] < 300)</Filter> 72 | <LineSymbolizer stroke-width="3" stroke="#04ff0a" stroke-linecap="butt" clip="false" /> 73 | </Rule> 74 | </Style> 75 | <Style name="l3_color" filter-mode="first"> 76 | <Rule> 77 | <LineSymbolizer stroke-width="3" stroke="#0684c4" stroke-linecap="butt" clip="false" /> 78 | </Rule> 79 | </Style> 80 | <Style name="shadow1"> 81 | <Rule> 82 | <LineSymbolizer stroke-width="4" stroke="#333333" stroke-linecap="round" stroke-opacity="0.5" /> 83 | </Rule> 84 | </Style> 85 | 86 | <Layer name="links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 87 | <StyleName>shadow1</StyleName> 88 | <StyleName>color</StyleName> 89 | <Datasource> 90 | <Parameter name="type">csv</Parameter> 91 | <Parameter name="file">csv/links.csv</Parameter> 92 | </Datasource> 93 | </Layer> 94 | <Layer name="l3_links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 95 | <StyleName>shadow1</StyleName> 96 | <StyleName>l3_color</StyleName> 97 | <Datasource> 98 | <Parameter name="type">csv</Parameter> 99 | <Parameter name="file">csv/l3_links.csv</Parameter> 100 | </Datasource> 101 | </Layer> 102 | <Layer name="routers" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 103 | <StyleName>routerpoint</StyleName> 104 | <Datasource> 105 | <Parameter name="type">csv</Parameter> 106 | <Parameter name="file">csv/routers.csv</Parameter> 107 | </Datasource> 108 | </Layer> 109 | </Map> 110 | -------------------------------------------------------------------------------- /ffmap/mapnik/routers_local.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!DOCTYPE Map> 3 | <Map background-color="transparent" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"> 4 | <Style name="routerpoint" filter-mode="first"> 5 | <Rule> 6 | <Filter>([status] = 'online')</Filter> 7 | <!-- For directed antenna 8 | <PointSymbolizer file="static/img/router_direct_green.svg" allow-overlap="true" transform="rotate(45)" /> 9 | --> 10 | <PointSymbolizer file="static/img/router_green_v2.svg" allow-overlap="true" /> 11 | </Rule> 12 | <Rule> 13 | <Filter>([status] = 'offline')</Filter> 14 | <PointSymbolizer file="static/img/router_red_v2.svg" allow-overlap="true" /> 15 | </Rule> 16 | <Rule> 17 | <Filter>([status] = 'unknown')</Filter> 18 | <PointSymbolizer file="static/img/router_yellow.svg" allow-overlap="true" /> 19 | </Rule> 20 | <Rule> 21 | <Filter>([status] = 'orphaned')</Filter> 22 | <PointSymbolizer file="static/img/router_grey.svg" allow-overlap="true" /> 23 | </Rule> 24 | <Rule> 25 | <Filter>([status] = 'online_wan')</Filter> 26 | <PointSymbolizer file="static/img/router_green_v2_white.svg" allow-overlap="true" /> 27 | </Rule> 28 | <Rule> 29 | <Filter>([status] = 'offline_wan')</Filter> 30 | <PointSymbolizer file="static/img/router_red_v2_white.svg" allow-overlap="true" /> 31 | </Rule> 32 | <Rule> 33 | <Filter>([status] = 'unknown_wan')</Filter> 34 | <PointSymbolizer file="static/img/router_yellow_white.svg" allow-overlap="true" /> 35 | </Rule> 36 | <Rule> 37 | <Filter>([status] = 'orphaned_wan')</Filter> 38 | <PointSymbolizer file="static/img/router_grey_white.svg" allow-overlap="true" /> 39 | </Rule> 40 | </Style> 41 | <Style name="color" filter-mode="first"> 42 | <Rule> 43 | <Filter>([quality] < 1)</Filter> 44 | <LineSymbolizer stroke-width="3" stroke="#008c00" stroke-linecap="butt" clip="false" /> 45 | </Rule> 46 | <Rule> 47 | <Filter>([quality] < 105)</Filter> 48 | <LineSymbolizer stroke-width="3" stroke="#ff1e1e" stroke-linecap="butt" clip="false" /> 49 | </Rule> 50 | <Rule> 51 | <Filter>([quality] < 130)</Filter> 52 | <LineSymbolizer stroke-width="3" stroke="#ff4949" stroke-linecap="butt" clip="false" /> 53 | </Rule> 54 | <Rule> 55 | <Filter>([quality] < 155)</Filter> 56 | <LineSymbolizer stroke-width="3" stroke="#ff6a6a" stroke-linecap="butt" clip="false" /> 57 | </Rule> 58 | <Rule> 59 | <Filter>([quality] < 180)</Filter> 60 | <LineSymbolizer stroke-width="3" stroke="#ffac53" stroke-linecap="butt" clip="false" /> 61 | </Rule> 62 | <Rule> 63 | <Filter>([quality] < 205)</Filter> 64 | <LineSymbolizer stroke-width="3" stroke="#ffeb79" stroke-linecap="butt" clip="false" /> 65 | </Rule> 66 | <Rule> 67 | <Filter>([quality] < 230)</Filter> 68 | <LineSymbolizer stroke-width="3" stroke="#79ff7c" stroke-linecap="butt" clip="false" /> 69 | </Rule> 70 | <Rule> 71 | <Filter>([quality] < 300)</Filter> 72 | <LineSymbolizer stroke-width="3" stroke="#04ff0a" stroke-linecap="butt" clip="false" /> 73 | </Rule> 74 | </Style> 75 | <Style name="l3_color" filter-mode="first"> 76 | <Rule> 77 | <LineSymbolizer stroke-width="3" stroke="#0684c4" stroke-linecap="butt" clip="false" /> 78 | </Rule> 79 | </Style> 80 | <Style name="shadow1"> 81 | <Rule> 82 | <LineSymbolizer stroke-width="4" stroke="#333333" stroke-linecap="round" stroke-opacity="0.5" /> 83 | </Rule> 84 | </Style> 85 | 86 | <Layer name="links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 87 | <StyleName>shadow1</StyleName> 88 | <StyleName>color</StyleName> 89 | <Datasource> 90 | <Parameter name="type">csv</Parameter> 91 | <Parameter name="file">csv/links_local.csv</Parameter> 92 | </Datasource> 93 | </Layer> 94 | <Layer name="l3_links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 95 | <StyleName>shadow1</StyleName> 96 | <StyleName>l3_color</StyleName> 97 | <Datasource> 98 | <Parameter name="type">csv</Parameter> 99 | <Parameter name="file">csv/l3_links_local.csv</Parameter> 100 | </Datasource> 101 | </Layer> 102 | <Layer name="routers" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 103 | <StyleName>routerpoint</StyleName> 104 | <Datasource> 105 | <Parameter name="type">csv</Parameter> 106 | <Parameter name="file">csv/routers_local.csv</Parameter> 107 | </Datasource> 108 | </Layer> 109 | </Map> 110 | -------------------------------------------------------------------------------- /ffmap/mapnik/routers_v2.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!DOCTYPE Map> 3 | <Map background-color="transparent" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"> 4 | <Style name="routerpoint" filter-mode="first"> 5 | <Rule> 6 | <Filter>([status] = 'online')</Filter> 7 | <!-- For directed antenna 8 | <PointSymbolizer file="static/img/router_direct_green.svg" allow-overlap="true" transform="rotate(45)" /> 9 | --> 10 | <PointSymbolizer file="static/img/router_green_v2.svg" allow-overlap="true" /> 11 | </Rule> 12 | <Rule> 13 | <Filter>([status] = 'offline')</Filter> 14 | <PointSymbolizer file="static/img/router_red_v2.svg" allow-overlap="true" /> 15 | </Rule> 16 | <Rule> 17 | <Filter>([status] = 'unknown')</Filter> 18 | <PointSymbolizer file="static/img/router_yellow.svg" allow-overlap="true" /> 19 | </Rule> 20 | <Rule> 21 | <Filter>([status] = 'orphaned')</Filter> 22 | <PointSymbolizer file="static/img/router_grey.svg" allow-overlap="true" /> 23 | </Rule> 24 | <Rule> 25 | <Filter>([status] = 'online_wan')</Filter> 26 | <PointSymbolizer file="static/img/router_green_v2_white.svg" allow-overlap="true" /> 27 | </Rule> 28 | <Rule> 29 | <Filter>([status] = 'offline_wan')</Filter> 30 | <PointSymbolizer file="static/img/router_red_v2_white.svg" allow-overlap="true" /> 31 | </Rule> 32 | <Rule> 33 | <Filter>([status] = 'unknown_wan')</Filter> 34 | <PointSymbolizer file="static/img/router_yellow_white.svg" allow-overlap="true" /> 35 | </Rule> 36 | <Rule> 37 | <Filter>([status] = 'orphaned_wan')</Filter> 38 | <PointSymbolizer file="static/img/router_grey_white.svg" allow-overlap="true" /> 39 | </Rule> 40 | </Style> 41 | <Style name="color" filter-mode="first"> 42 | <Rule> 43 | <Filter>([quality] < 1)</Filter> 44 | <LineSymbolizer stroke-width="3" stroke="#008c00" stroke-linecap="butt" clip="false" /> 45 | </Rule> 46 | <Rule> 47 | <Filter>([quality] < 105)</Filter> 48 | <LineSymbolizer stroke-width="3" stroke="#ff1e1e" stroke-linecap="butt" clip="false" /> 49 | </Rule> 50 | <Rule> 51 | <Filter>([quality] < 130)</Filter> 52 | <LineSymbolizer stroke-width="3" stroke="#ff4949" stroke-linecap="butt" clip="false" /> 53 | </Rule> 54 | <Rule> 55 | <Filter>([quality] < 155)</Filter> 56 | <LineSymbolizer stroke-width="3" stroke="#ff6a6a" stroke-linecap="butt" clip="false" /> 57 | </Rule> 58 | <Rule> 59 | <Filter>([quality] < 180)</Filter> 60 | <LineSymbolizer stroke-width="3" stroke="#ffac53" stroke-linecap="butt" clip="false" /> 61 | </Rule> 62 | <Rule> 63 | <Filter>([quality] < 205)</Filter> 64 | <LineSymbolizer stroke-width="3" stroke="#ffeb79" stroke-linecap="butt" clip="false" /> 65 | </Rule> 66 | <Rule> 67 | <Filter>([quality] < 230)</Filter> 68 | <LineSymbolizer stroke-width="3" stroke="#79ff7c" stroke-linecap="butt" clip="false" /> 69 | </Rule> 70 | <Rule> 71 | <Filter>([quality] < 300)</Filter> 72 | <LineSymbolizer stroke-width="3" stroke="#04ff0a" stroke-linecap="butt" clip="false" /> 73 | </Rule> 74 | </Style> 75 | <Style name="l3_color" filter-mode="first"> 76 | <Rule> 77 | <LineSymbolizer stroke-width="3" stroke="#0684c4" stroke-linecap="butt" clip="false" /> 78 | </Rule> 79 | </Style> 80 | <Style name="shadow1"> 81 | <Rule> 82 | <LineSymbolizer stroke-width="4" stroke="#333333" stroke-linecap="round" stroke-opacity="0.5" /> 83 | </Rule> 84 | </Style> 85 | 86 | <Layer name="links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 87 | <StyleName>shadow1</StyleName> 88 | <StyleName>color</StyleName> 89 | <Datasource> 90 | <Parameter name="type">csv</Parameter> 91 | <Parameter name="file">csv/links_v2.csv</Parameter> 92 | </Datasource> 93 | </Layer> 94 | <Layer name="l3_links" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 95 | <StyleName>shadow1</StyleName> 96 | <StyleName>l3_color</StyleName> 97 | <Datasource> 98 | <Parameter name="type">csv</Parameter> 99 | <Parameter name="file">csv/l3_links_v2.csv</Parameter> 100 | </Datasource> 101 | </Layer> 102 | <Layer name="routers" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"> 103 | <StyleName>routerpoint</StyleName> 104 | <Datasource> 105 | <Parameter name="type">csv</Parameter> 106 | <Parameter name="file">csv/routers_v2.csv</Parameter> 107 | </Datasource> 108 | </Layer> 109 | </Map> 110 | -------------------------------------------------------------------------------- /ffmap/mapnik/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | liteserv.py routers.xml --processes=5 & 4 | liteserv.py routers_v2.xml -p 8003 --processes=5 & 5 | liteserv.py routers_local.xml -p 8004 --processes=5 & 6 | liteserv.py hoods_v2.xml -p 8002 --processes=5 7 | liteserv.py hoods_poly.xml -p 8005 --processes=5 8 | 9 | killall liteserv.py 10 | -------------------------------------------------------------------------------- /ffmap/mapnik/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from distutils.core import setup 4 | setup(name='dynmapnik', 5 | version='1.0', 6 | py_modules=['dynmapnik'], 7 | ) 8 | -------------------------------------------------------------------------------- /ffmap/mapnik/tilestache.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "name": "Disk", 4 | "path": "/var/cache/ffmap/tiles/" 5 | }, 6 | "layers": { 7 | "tiles/routers": { 8 | "provider": { 9 | "class": "dynmapnik:DynMapnik", 10 | "kwargs": { 11 | "mapfile": "/usr/share/ffmap/routers.xml" 12 | } 13 | }, 14 | "metatile": {"buffer": 128}, 15 | "cache lifespan": 300 16 | }, 17 | "tiles/routers_v2": { 18 | "provider": { 19 | "class": "dynmapnik:DynMapnik", 20 | "kwargs": { 21 | "mapfile": "/usr/share/ffmap/routers_v2.xml" 22 | } 23 | }, 24 | "metatile": {"buffer": 128}, 25 | "cache lifespan": 300 26 | }, 27 | "tiles/routers_local": { 28 | "provider": { 29 | "class": "dynmapnik:DynMapnik", 30 | "kwargs": { 31 | "mapfile": "/usr/share/ffmap/routers_local.xml" 32 | } 33 | }, 34 | "metatile": {"buffer": 128}, 35 | "cache lifespan": 300 36 | }, 37 | "tiles/hoods_v2": { 38 | "provider": { 39 | "class": "dynmapnik:DynMapnik", 40 | "kwargs": { 41 | "mapfile": "/usr/share/ffmap/hoods_v2.xml" 42 | } 43 | }, 44 | "metatile": {"buffer": 128}, 45 | "cache lifespan": 300 46 | }, 47 | "tiles/hoods_poly": { 48 | "provider": { 49 | "class": "dynmapnik:DynMapnik", 50 | "kwargs": { 51 | "mapfile": "/usr/share/ffmap/hoods_poly.xml" 52 | } 53 | }, 54 | "metatile": {"buffer": 128}, 55 | "cache lifespan": 300 56 | } 57 | }, 58 | "logging": "info" 59 | } 60 | -------------------------------------------------------------------------------- /ffmap/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import time 4 | import datetime 5 | 6 | from ffmap.config import CONFIG 7 | #from socket import inet_pton, inet_ntop, AF_INET6 8 | from ipaddress import IPv4Address, IPv6Address 9 | 10 | ipv6local = IPv6Address('fc00::') 11 | 12 | def utcnow(): 13 | return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) 14 | 15 | def int2mac(data,keys=None): 16 | if keys: 17 | for k in keys: 18 | data[k] = int2mac(data[k]) 19 | return data 20 | if data: 21 | return ':'.join(format(s, '02x') for s in data.to_bytes(6,byteorder='big')) 22 | #return ':'.join(format(s, '02x') for s in bytes.fromhex('{0:x}'.format(data))) 23 | else: 24 | return '' 25 | 26 | def int2shortmac(data,keys=None): 27 | if keys: 28 | for k in keys: 29 | data[k] = int2shortmac(data[k]) 30 | return data 31 | if data: 32 | return '{:012x}'.format(data) 33 | else: 34 | return '' 35 | 36 | def shortmac2mac(data): 37 | if data: 38 | return ':'.join(format(s, '02x') for s in bytes.fromhex(data.replace(':',''))) 39 | else: 40 | return '' 41 | 42 | def mac2int(data): 43 | if data: 44 | return int(data.replace(":",""),16) 45 | else: 46 | return None 47 | 48 | def int2mactuple(data,index=None): 49 | if index: 50 | for r in data: 51 | r[index] = int2mac(r[index]) 52 | else: 53 | for r in data: 54 | r = int2mac(r) 55 | return data 56 | 57 | def ipv6tobin(data): 58 | if data: 59 | return IPv6Address(data).packed 60 | #return inet_pton(AF_INET6,data) 61 | else: 62 | return None 63 | 64 | def ipv6tobinmasked(data): 65 | if data: 66 | ip = IPv6Address(data) 67 | if ip >= ipv6local: 68 | return ip.packed 69 | else: 70 | li = list(ip.packed) 71 | # mask 1234:1234:ffff:ffff:ffff:ffff:ffff:ff34 72 | li[4:15] = [255,255,255,255,255,255,255,255,255,255,255] 73 | return IPv6Address(bytes(li)).packed 74 | else: 75 | return None 76 | 77 | def bintoipv6(data): 78 | if data: 79 | return IPv6Address(data).compressed 80 | #return inet_ntop(AF_INET6,data) 81 | else: 82 | return '' 83 | 84 | def ipv4toint(data): 85 | if data: 86 | return int(IPv4Address(data)) 87 | #return inet_pton(AF_INET,data) 88 | else: 89 | return None 90 | def inttoipv4(data): 91 | if data: 92 | return str(IPv4Address(data)) 93 | #return inet_ntop(AF_INET,data) 94 | else: 95 | return '' 96 | 97 | def writelog(path, content): 98 | with open(path, "a") as csv: 99 | csv.write(time.strftime('{%Y-%m-%d %H:%M:%S}') + " - " + content + "\n") 100 | 101 | def writefulllog(content): 102 | with open(CONFIG["debug_dir"] + "/fulllog.log", "a") as csv: 103 | csv.write(time.strftime('{%Y-%m-%d %H:%M:%S}') + " - " + content + "\n") 104 | 105 | def neighbor_color(quality,netif,rt_protocol): 106 | color = "#04ff0a" 107 | if rt_protocol=="BATMAN_V": 108 | if quality < 10: 109 | color = "#ff1e1e" 110 | elif quality < 20: 111 | color = "#ff4949" 112 | elif quality < 40: 113 | color = "#ff6a6a" 114 | elif quality < 80: 115 | color = "#ffac53" 116 | elif quality < 1000: 117 | color = "#ffeb79" 118 | else: 119 | if quality < 105: 120 | color = "#ff1e1e" 121 | elif quality < 130: 122 | color = "#ff4949" 123 | elif quality < 155: 124 | color = "#ff6a6a" 125 | elif quality < 180: 126 | color = "#ffac53" 127 | elif quality < 205: 128 | color = "#ffeb79" 129 | elif quality < 230: 130 | color = "#79ff7c" 131 | if netif.startswith("eth"): 132 | #color = "#999999" 133 | color = "#008c00" 134 | if quality < 0: 135 | color = "#06a4f4" 136 | return color 137 | 138 | def defrag_table(mysql,table,sleep): 139 | minustime=0 140 | allrows=0 141 | start_time = time.time() 142 | 143 | qry = "ALTER TABLE `%s` ENGINE = InnoDB" % (table) 144 | mysql.execute(qry) 145 | mysql.commit() 146 | 147 | end_time = time.time() 148 | if sleep > 0: 149 | time.sleep(sleep) 150 | 151 | writelog(CONFIG["debug_dir"] + "/deletetime.txt", "Defragmented table %s: %.3f seconds" % (table,end_time - start_time)) 152 | print("--- Defragmented table %s: %.3f seconds ---" % (table,end_time - start_time)) 153 | 154 | def defrag_all(mysql,doall=False): 155 | alltables = ('gw','gw_admin','gw_netif','hoods','hoodsv2','netifs','router','router_events','router_gw','router_ipv6','router_neighbor','router_netif','users') 156 | stattables = ('router_stats','router_stats_gw','router_stats_neighbor','router_stats_netif','stats_global','stats_gw','stats_hood') 157 | 158 | for t in alltables: 159 | defrag_table(mysql,t,1) 160 | 161 | if doall: 162 | for t in stattables: 163 | defrag_table(mysql,t,60) 164 | 165 | writelog(CONFIG["debug_dir"] + "/deletetime.txt", "-------") 166 | -------------------------------------------------------------------------------- /ffmap/mysqlconfig.example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | mysq = { 4 | "host":"localhost", 5 | "user":"root", 6 | "passwd":"password", 7 | "db":"dbname" 8 | } 9 | -------------------------------------------------------------------------------- /ffmap/mysqltools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import MySQLdb 4 | from ffmap.mysqlconfig import mysq 5 | from ffmap.misc import * 6 | import datetime 7 | 8 | #import pytz 9 | 10 | class FreifunkMySQL: 11 | 12 | db = None 13 | cur = None 14 | 15 | def __init__(self): 16 | #global mysq 17 | self.db = MySQLdb.connect(host=mysq["host"], user=mysq["user"], passwd=mysq["passwd"], db=mysq["db"], charset="utf8") 18 | #self.db.set_character_set('utf8') 19 | self.cur = self.db.cursor(MySQLdb.cursors.DictCursor) 20 | 21 | def close(self): 22 | self.db.close() 23 | 24 | def cursor(self): 25 | return self.cur 26 | 27 | def commit(self): 28 | self.db.commit() 29 | 30 | def fetchall(self,str,tup=(),key=None): 31 | self.cur.execute(str,tup) 32 | result = self.cur.fetchall() 33 | if len(result) > 0: 34 | if key: 35 | rnew = [] 36 | for r in result: 37 | rnew.append(r[key]) 38 | return rnew 39 | else: 40 | return result 41 | else: 42 | return () 43 | 44 | def fetchdict(self,str,tup,key,value=None): 45 | self.cur.execute(str,tup) 46 | dict = {} 47 | for d in self.cur.fetchall(): 48 | if value: 49 | dict[d[key]] = d[value] 50 | else: 51 | dict[d[key]] = d 52 | return dict 53 | 54 | def findone(self,str,tup,sel=None): 55 | self.cur.execute(str,tup) 56 | result = self.cur.fetchall() 57 | if len(result) > 0: 58 | if sel: 59 | return result[0][sel] 60 | else: 61 | return result[0] 62 | else: 63 | return False 64 | 65 | def executemany(self,a,b): 66 | if not b: 67 | return 0 68 | return self.cur.executemany(a,b) 69 | 70 | def execute(self,a,b=None): 71 | if b: 72 | return self.cur.execute(a,b) 73 | else: 74 | return self.cur.execute(a) 75 | 76 | def utcnow(self): 77 | return utcnow().strftime('%Y-%m-%d %H:%M:%S') 78 | 79 | def utctimestamp(self): 80 | return int(utcnow().timestamp()) 81 | 82 | def formatdt(self,dt): 83 | return dt.strftime('%Y-%m-%d %H:%M:%S') 84 | 85 | def formattimestamp(self,t): 86 | return int(t.timestamp()) 87 | 88 | def utcawareint(self,data,keys=None): 89 | if keys: 90 | for k in keys: 91 | data[k] = datetime.datetime.fromtimestamp(data[k],datetime.timezone.utc) 92 | else: 93 | data = datetime.datetime.fromtimestamp(data,datetime.timezone.utc) 94 | return data 95 | 96 | def utcawaretupleint(self,data,index=None): 97 | if index: 98 | for r in data: 99 | r[index] = datetime.datetime.fromtimestamp(r[index],datetime.timezone.utc) 100 | else: 101 | for r in data: 102 | r = datetime.datetime.fromtimestamp(r,datetime.timezone.utc) 103 | return data 104 | 105 | def utcaware(self,data,keys=None): 106 | if keys: 107 | for k in keys: 108 | #self.utcaware(data[k]) 109 | #data[k] = pytz.utc.localize(data[k]) 110 | data[k] = data[k].replace(tzinfo=datetime.timezone.utc) 111 | else: 112 | #data = pytz.utc.localize(data) 113 | data = data.replace(tzinfo=datetime.timezone.utc) 114 | return data 115 | 116 | def utcawaretuple(self,data,index=None): 117 | if index: 118 | for r in data: 119 | #self.utcaware(r[index]) 120 | #r[index] = pytz.utc.localize(r[index]) 121 | r[index] = r[index].replace(tzinfo=datetime.timezone.utc) 122 | else: 123 | for r in data: 124 | #self.utcaware(r) 125 | #r = pytz.utc.localize(r) 126 | r = r.replace(tzinfo=datetime.timezone.utc) 127 | return data 128 | -------------------------------------------------------------------------------- /ffmap/systemd/uwsgi-ffmap.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FF-MAP Web UI 3 | After=syslog.target 4 | 5 | [Service] 6 | ExecStart=/usr/bin/uwsgi_python3 -s 127.0.0.1:3031 -w ffmap.web.application:app --master --processes 4 --enable-threads --uid www-data --gid www-data --catch-exceptions --disable-logging --log-4xx --log-5xx 7 | Restart=always 8 | KillSignal=SIGQUIT 9 | Type=notify 10 | StandardError=syslog 11 | NotifyAccess=all 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ffmap/systemd/uwsgi-tiles.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FF-MAP Tiles 3 | After=syslog.target 4 | 5 | [Service] 6 | ExecStart=/usr/bin/uwsgi_python3 -s 127.0.0.1:3032 --eval 'import sys; sys.path.insert(0,"/data/fff/TileStache"); import TileStache; application = TileStache.WSGITileServer("/usr/share/ffmap/tilestache.cfg")' --master --processes 4 --uid www-data --gid www-data --enable-threads --disable-logging --log-4xx --log-5xx 7 | Restart=always 8 | KillSignal=SIGQUIT 9 | Type=notify 10 | StandardError=syslog 11 | NotifyAccess=all 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ffmap/usertools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | from ffmap.misc import * 9 | 10 | from werkzeug.security import generate_password_hash, check_password_hash 11 | 12 | class AccountWithEmptyField(Exception): 13 | pass 14 | 15 | class AccountWithEmailExists(Exception): 16 | pass 17 | 18 | class AccountWithNicknameExists(Exception): 19 | pass 20 | 21 | class AccountNotExisting(Exception): 22 | pass 23 | 24 | class InvalidToken(Exception): 25 | pass 26 | 27 | def register_user(nickname, email, password): 28 | if not nickname or not email: 29 | raise AccountWithEmptyField() 30 | 31 | mysql = FreifunkMySQL() 32 | user_with_nick = mysql.findone("SELECT id, email FROM users WHERE nickname = %s LIMIT 1",(nickname,)) 33 | user_with_email = mysql.findone("SELECT id FROM users WHERE email = %s LIMIT 1",(email,),"id") 34 | pw = generate_password_hash(password) 35 | if user_with_email: 36 | mysql.close() 37 | raise AccountWithEmailExists() 38 | elif user_with_nick and user_with_nick["email"]: 39 | mysql.close() 40 | raise AccountWithNicknameExists() 41 | else: 42 | time = mysql.utcnow() 43 | if user_with_nick: 44 | mysql.execute(""" 45 | UPDATE users 46 | SET password = %s, email = %s, created = %s, token = NULL 47 | WHERE id = %s 48 | LIMIT 1 49 | """,(pw,email,time,user_with_nick["id"],)) 50 | mysql.commit() 51 | mysql.close() 52 | return user_with_nick["id"] 53 | else: 54 | mysql.execute(""" 55 | INSERT INTO users (nickname, password, email, created, token) 56 | VALUES (%s, %s, %s, %s, NULL) 57 | """,(nickname,pw,email,time,)) 58 | userid = mysql.cursor().lastrowid 59 | mysql.commit() 60 | mysql.close() 61 | return userid 62 | 63 | def check_login_details(nickname, password): 64 | mysql = FreifunkMySQL() 65 | user = mysql.findone("SELECT * FROM users WHERE nickname = %s LIMIT 1",(nickname,)) 66 | userbymail = mysql.findone("SELECT * FROM users WHERE email = %s LIMIT 1",(nickname,)) 67 | mysql.close() 68 | 69 | if user and check_password_hash(user.get('password', ''), password): 70 | return user 71 | elif userbymail and check_password_hash(userbymail.get('password', ''), password): 72 | return userbymail 73 | return False 74 | 75 | def reset_user_password(mysql, email, token=None, password=None): 76 | user = mysql.findone("SELECT id, nickname, token FROM users WHERE email = %s LIMIT 1",(email,)) 77 | if not user: 78 | raise AccountNotExisting() 79 | elif password: 80 | if user.get("token") == token: 81 | mysql.execute(""" 82 | UPDATE users 83 | SET password = %s, token = NULL 84 | WHERE id = %s 85 | LIMIT 1 86 | """,(generate_password_hash(password),user["id"],)) 87 | mysql.commit() 88 | else: 89 | raise InvalidToken() 90 | elif token: 91 | mysql.execute(""" 92 | UPDATE users 93 | SET token = %s 94 | WHERE id = %s 95 | LIMIT 1 96 | """,(token,user["id"],)) 97 | mysql.commit() 98 | return user 99 | 100 | def set_user_password(mysql, nickname, password): 101 | userid = mysql.findone("SELECT id FROM users WHERE nickname = %s LIMIT 1",(nickname,),"id") 102 | if not userid: 103 | raise AccountNotExisting() 104 | elif password: 105 | mysql.execute(""" 106 | UPDATE users 107 | SET password = %s 108 | WHERE id = %s 109 | LIMIT 1 110 | """,(generate_password_hash(password),userid,)) 111 | mysql.commit() 112 | 113 | def set_user_email(mysql, nickname, email): 114 | userid = mysql.findone("SELECT id FROM users WHERE nickname = %s LIMIT 1",(nickname,),"id") 115 | useridemail = mysql.findone("SELECT id FROM users WHERE email = %s LIMIT 1",(email,),"id") 116 | if useridemail: 117 | raise AccountWithEmailExists() 118 | if not userid: 119 | raise AccountNotExisting() 120 | elif email: 121 | mysql.execute(""" 122 | UPDATE users 123 | SET email = %s 124 | WHERE id = %s 125 | LIMIT 1 126 | """,(email,userid,)) 127 | mysql.commit() 128 | 129 | def set_user_admin(mysql, nickname, admin): 130 | mysql.execute(""" 131 | UPDATE users 132 | SET admin = %s 133 | WHERE nickname = %s 134 | LIMIT 1 135 | """,(admin,nickname,)) 136 | mysql.commit() 137 | 138 | def set_user_abuse(mysql, nickname, abuse): 139 | mysql.execute(""" 140 | UPDATE users 141 | SET abuse = %s 142 | WHERE nickname = %s 143 | LIMIT 1 144 | """,(abuse,nickname,)) 145 | mysql.commit() 146 | 147 | def users_v2(mysql): 148 | data = mysql.fetchall(""" 149 | SELECT contact, COUNT(id) AS count, v2 150 | FROM router 151 | GROUP BY contact, v2 152 | """) 153 | 154 | datasort = {} 155 | for d in data: 156 | contact = d["contact"].lower() 157 | if not contact in datasort: 158 | datasort[contact] = {"v2":0, "v1":0} 159 | if d["v2"]: 160 | datasort[contact]["v2"] = d["count"] 161 | else: 162 | datasort[contact]["v1"] = d["count"] 163 | 164 | return datasort 165 | 166 | -------------------------------------------------------------------------------- /ffmap/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/__init__.py -------------------------------------------------------------------------------- /ffmap/web/filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from flask import Blueprint, session 4 | from dateutil import tz 5 | from bson.json_util import dumps as bson2json 6 | import os 7 | import sys 8 | import json 9 | import datetime 10 | import re 11 | import hashlib 12 | from ffmap.misc import int2mac, int2shortmac, inttoipv4, bintoipv6 13 | from ipaddress import ip_address 14 | 15 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '../..')) 16 | from ffmap.misc import * 17 | 18 | filters = Blueprint("filters", __name__) 19 | 20 | @filters.app_template_filter('sumdict') 21 | def sumdict(d): 22 | return sum(d.values()) 23 | 24 | @filters.app_template_filter('v2userpercent') 25 | def v2formatpercent(d): 26 | return "{:.0f}".format(v2numberpercent(d)) 27 | 28 | def v2numberpercent(d): 29 | if d.get("v1",0) > 0 or d.get("v2",0) > 0: 30 | return d["v2"] * 100 / ( d["v1"] + d["v2"] ) 31 | else: 32 | return 0.0 33 | 34 | @filters.app_template_filter('v2colorpercent') 35 | def v2colorpercent(d): 36 | pc = v2numberpercent(d) 37 | color = "000000" 38 | if pc > 99: 39 | color = "008800" 40 | elif pc > 75: 41 | color = "00d93d" 42 | elif pc > 50: 43 | color = "ffc926" 44 | elif pc > 25: 45 | color = "ff9326" 46 | elif pc > 1: 47 | color = "ff0000" 48 | return "color:#" + color 49 | 50 | @filters.app_template_filter('longip') 51 | def longip(d): 52 | if len(d) > 32: 53 | return d.replace('::','::... ...::') 54 | else: 55 | return d 56 | 57 | @filters.app_template_filter('int2mac') 58 | def int2macfilter(d): 59 | return int2mac(d) 60 | 61 | @filters.app_template_filter('int2shortmac') 62 | def int2shortmacfilter(d): 63 | return int2shortmac(d) 64 | 65 | @filters.app_template_filter('int2ipv4') 66 | def int2ipv4filter(d): 67 | return inttoipv4(d) 68 | 69 | @filters.app_template_filter('bin2ipv6') 70 | def bin2ipv6filter(d): 71 | return bintoipv6(d) 72 | 73 | @filters.app_template_filter('ip2int') 74 | def ip2intfilter(d): 75 | try: 76 | return int(ip_address(d)) 77 | except ValueError as e: 78 | return 0 79 | 80 | @filters.app_template_filter('ipnet2int') 81 | def ipnet2intfilter(d): 82 | try: 83 | return int(ip_address(d.split("/")[0])) 84 | except ValueError as e: 85 | return 0 86 | 87 | @filters.app_template_filter('utc2local') 88 | def utc2local(dt): 89 | return dt.astimezone(tz.tzlocal()) 90 | 91 | @filters.app_template_filter('format_dt') 92 | def format_dt(dt): 93 | return dt.strftime("%Y-%m-%d %H:%M:%S") 94 | 95 | @filters.app_template_filter('format_dt_date') 96 | def format_dt_date(dt): 97 | return dt.strftime("%Y-%m-%d") 98 | 99 | @filters.app_template_filter('dt2jstimestamp') 100 | def dt2jstimestamp(dt): 101 | return int(dt.timestamp())*1000 102 | 103 | @filters.app_template_filter('format_dt_ago') 104 | def format_dt_ago(dt): 105 | diff = utcnow() - dt 106 | s = diff.seconds 107 | if diff.days > 1: 108 | return '%i days ago' % diff.days 109 | elif diff.days == 1: 110 | return '1 day ago' 111 | elif s <= 1: 112 | return 'just now' 113 | elif s < 60: 114 | return '%i seconds ago' % s 115 | elif s < 120: 116 | return '1 minute ago' 117 | elif s < 3600: 118 | return '%i minutes ago' % (s/60) 119 | elif s < 7200: 120 | return '1 hour ago' 121 | else: 122 | return '%i hours ago' % (s/3600) 123 | 124 | @filters.app_template_filter('format_ts_diff') 125 | def format_dt_diff(ts): 126 | diff = datetime.timedelta(seconds=ts) 127 | s = diff.seconds 128 | if diff.days > 1: 129 | return '%i days' % diff.days 130 | elif diff.days == 1: 131 | return '1 day' 132 | elif s <= 1: 133 | return '< 1 sec' 134 | elif s < 60: 135 | return '%i sec' % s 136 | elif s < 120: 137 | return '1 min' 138 | elif s < 3600: 139 | return '%i min' % (s/60) 140 | elif s < 7200: 141 | return '1 hour' 142 | else: 143 | return '%i hours' % (s/3600) 144 | 145 | @filters.app_template_filter('bson2json') 146 | def bson_to_json(bsn): 147 | return bson2json(bsn) 148 | 149 | @filters.app_template_filter('statbson2json') 150 | def statbson_to_json(bsn): 151 | for point in bsn: 152 | point["time"] = {"$date": int(point["time"].timestamp()*1000)} 153 | return json.dumps(bsn) 154 | 155 | @filters.app_template_filter('nbsp') 156 | def nbsp(txt): 157 | return txt.replace(" ", " ") 158 | 159 | @filters.app_template_filter('humanize_bytes') 160 | def humanize_bytes(num, suffix='B'): 161 | for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: 162 | if abs(num) < 1024.0 and unit != '': 163 | return "%3.1f %s%s" % (num, unit, suffix) 164 | num /= 1024.0 165 | return "%.1f %s%s" % (num, 'Yi', suffix) 166 | 167 | @filters.app_template_filter('bytes_to_bits') 168 | def bytes_to_bits(num, suffix='b'): 169 | num *= 8.0 170 | for unit in ['','k','M','G','T','P','E','Z']: 171 | if abs(num) < 1000.0 and unit != '': 172 | return "%3.1f %s%s" % (num, unit, suffix) 173 | num /= 1000.0 174 | return "%.1f %s%s" % (num, 'Y', suffix) 175 | 176 | @filters.app_template_filter('mac2fe80') 177 | def mac_to_ipv6_linklocal(mac): 178 | if not mac: 179 | return '' 180 | 181 | # Remove the most common delimiters; dots, dashes, etc. 182 | mac_bare = re.sub('[%s]+' % re.escape(' .:-'), '', mac) 183 | return macint_to_ipv6_linklocal(int(mac_bare, 16)) 184 | 185 | @filters.app_template_filter('macint2fe80') 186 | def macint_to_ipv6_linklocal(mac_value): 187 | if not mac_value: 188 | return '' 189 | 190 | # Split out the bytes that slot into the IPv6 address 191 | # XOR the most significant byte with 0x02, inverting the 192 | # Universal / Local bit 193 | high2 = mac_value >> 32 & 0xffff ^ 0x0200 194 | high1 = mac_value >> 24 & 0xff 195 | low1 = mac_value >> 16 & 0xff 196 | low2 = mac_value & 0xffff 197 | 198 | return 'fe80::{:x}:{:x}ff:fe{:x}:{:x}'.format(high2, high1, low1, low2) 199 | 200 | @filters.app_template_filter('status2css') 201 | def status2css(status): 202 | status_map = { 203 | "offline": "danger", 204 | "unknown": "warning", 205 | "online": "success", 206 | "reboot": "info", 207 | "created": "primary", 208 | "netmon": "primary", 209 | "update": "primary", 210 | "orphaned": "default", 211 | "admin": "warning", 212 | } 213 | return "label label-%s" % status_map.get(status, "default") 214 | 215 | @filters.app_template_filter('anon_email') 216 | def anon_email(email, replacement_char='.'): 217 | if 'user' in session: 218 | return email 219 | 220 | try: 221 | def anon_str(s, full=False): 222 | if full: 223 | return replacement_char * len(s) 224 | else: 225 | hide_pos = int(len(s)/2) 226 | return s[:hide_pos] + replacement_char + s[(hide_pos+1):] 227 | prefix, tld = email.rsplit('.', 1) 228 | user, domain = prefix.split('@') 229 | return '%s@%s.%s' % (anon_str(user), anon_str(domain), anon_str(tld, True)) 230 | except: 231 | return email 232 | 233 | @filters.app_template_filter('anon_email_regex') 234 | def anon_email_regex(email): 235 | return anon_email(email, '*').replace('.', '\.').replace('*', '.').replace('+', '\+').replace('_', '\_') 236 | 237 | @filters.app_template_filter('gravatar_url') 238 | def gravatar_url(email): 239 | return "https://www.gravatar.com/avatar/%s?d=identicon" % hashlib.md5(email.encode("UTF-8").lower()).hexdigest() 240 | 241 | @filters.app_template_filter('webui_addr') 242 | def webui_addr(router_netifs): 243 | for br_mesh in filter(lambda n: n["netif"] == "br-mesh", router_netifs): 244 | for ipv6 in br_mesh["ipv6_addrs"]: 245 | ipv6 = bintoipv6(ipv6) 246 | if not ipv6: 247 | return None 248 | if ipv6.startswith("fd43"): 249 | # This selects the first ULA address, if present 250 | return ipv6 251 | if ipv6.startswith("fdff") and len(ipv6) > 10: 252 | # This selects the first fdff address, if present (and skips fdff::1) 253 | return ipv6 254 | return None 255 | 256 | @filters.app_template_filter('format_airtime') 257 | def format_airtime(airtime): 258 | return "%.0f %%" % (airtime*100) 259 | 260 | @filters.app_template_filter('format_query') 261 | def format_query(query): 262 | return query.replace(" ","_").replace(".","\.").replace("(","\(").replace(")","\)") 263 | -------------------------------------------------------------------------------- /ffmap/web/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import bson 4 | import re 5 | import smtplib 6 | from email.mime.text import MIMEText 7 | 8 | def format_query(query_usr): 9 | query_list = [] 10 | for key, value in query_usr.items(): 11 | if key == "hostname": 12 | qtag = "" 13 | else: 14 | qtag = "%s:" % key 15 | query_list.append("%s%s" % (qtag, value)) 16 | return " ".join(query_list) 17 | 18 | allowed_filters = ( 19 | 'status', 20 | 'hood', 21 | 'nickname', 22 | 'hardware', 23 | 'firmware', 24 | 'mac', 25 | 'hostname', 26 | 'contact', 27 | 'community', 28 | 'neighbor', 29 | 'neighbour', 30 | 'gw', 31 | 'selected', 32 | 'bat', 33 | 'batselected', 34 | 'network', 35 | 'os', 36 | 'batman', 37 | 'kernel', 38 | 'nodewatcher', 39 | ) 40 | 41 | def parse_router_list_search_query(args): 42 | query_usr = bson.SON() 43 | if "q" in args: 44 | for word in args["q"].strip().split(" "): 45 | if not word: 46 | # Case of "q=" without arguments 47 | break 48 | if not ':' in word: 49 | key = "hostname" 50 | value = word 51 | else: 52 | key, value = word.split(':', 1) 53 | if key in allowed_filters: 54 | query_usr[key] = query_usr.get(key, "") + value 55 | s = "" 56 | j = "" 57 | t = [] 58 | i = 0 59 | for key, value in query_usr.items(): 60 | if i==0: 61 | prefix = " WHERE " 62 | else: 63 | prefix = " AND " 64 | if value.startswith('!'): 65 | no = "NOT " 66 | value = value[1:] 67 | else: 68 | no = "" 69 | 70 | if value == "EXISTS": 71 | k = key + ' <> "" AND ' + key + " IS NOT NULL" 72 | elif value == "EXISTS_NOT": 73 | k = key + ' = "" OR ' + key + " IS NULL" 74 | elif key == 'mac': 75 | j += " INNER JOIN ( SELECT router, mac FROM router_netif GROUP BY router, mac) AS j ON router.id = j.router " 76 | k = "HEX(mac) {} REGEXP %s".format(no) 77 | t.append(value.replace(':','')) 78 | elif (key == 'gw'): 79 | j += " INNER JOIN router_gw ON router.id = router_gw.router " 80 | k = "HEX(router_gw.mac) {} REGEXP %s".format(no) 81 | t.append(value.replace(':','')) 82 | elif (key == 'selected'): 83 | j += " INNER JOIN router_gw ON router.id = router_gw.router " 84 | k = "HEX(router_gw.mac) {} REGEXP %s AND router_gw.selected = TRUE".format(no) 85 | t.append(value.replace(':','')) 86 | elif (key == 'bat'): 87 | j += """ INNER JOIN router_gw ON router.id = router_gw.router 88 | INNER JOIN ( 89 | gw_netif AS n1 90 | INNER JOIN gw_netif AS n2 ON n1.mac = n2.vpnmac AND n1.gw = n2.gw 91 | ) ON router_gw.mac = n1.mac 92 | """ 93 | k = "HEX(n2.mac) {} REGEXP %s".format(no) 94 | t.append(value.replace(':','')) 95 | elif (key == 'batselected'): 96 | j += """ INNER JOIN router_gw ON router.id = router_gw.router 97 | INNER JOIN ( 98 | gw_netif AS n1 99 | INNER JOIN gw_netif AS n2 ON n1.mac = n2.vpnmac AND n1.gw = n2.gw 100 | ) ON router_gw.mac = n1.mac 101 | """ 102 | k = "HEX(n2.mac) {} REGEXP %s AND router_gw.selected = TRUE".format(no) 103 | t.append(value.replace(':','')) 104 | elif (key == 'neighbor') or (key == 'neighbour'): 105 | j += " INNER JOIN ( SELECT router, mac FROM router_neighbor GROUP BY router, mac) AS j ON router.id = j.router " 106 | k = "HEX(mac) {} REGEXP %s".format(no) 107 | t.append(value.replace(':','')) 108 | elif (key == 'hood'): 109 | k = "hoods.name {} REGEXP %s".format(no) 110 | t.append(value.replace("_",".")) 111 | elif (key == 'hardware') or (key == 'nickname'): 112 | k = key + " {} REGEXP %s".format(no) 113 | t.append(value.replace("_",".")) 114 | elif (key == 'hostname') or (key == 'firmware'): 115 | k = key + " {} REGEXP %s".format(no) 116 | t.append(value) 117 | elif key == 'contact': 118 | k = "contact {} REGEXP %s".format(no) 119 | t.append(value) 120 | elif key == 'network': 121 | # local hood included for v2 122 | if value.lower() == 'local': 123 | k = no + " (router.v2 = TRUE AND local = TRUE)" 124 | elif value.lower() == 'v2': 125 | k = no + " (router.v2 = TRUE AND local = FALSE)" 126 | elif value.lower() == 'v1': 127 | k = no + " router.v2 = FALSE" 128 | else: 129 | continue 130 | elif key in ('os','batman','kernel','nodewatcher',): 131 | k = key + " {} REGEXP %s".format(no) 132 | t.append(value.replace("_",".")) 133 | else: 134 | k = no + key + " = %s" 135 | t.append(value) 136 | i += 1 137 | s += prefix + k 138 | where = j + " " + s 139 | return (where, tuple(t), format_query(query_usr)) 140 | 141 | def send_email(recipient, subject, content, sender="FFF Monitoring <noreply@monitoring.freifunk-franken.de>"): 142 | msg = MIMEText(content) 143 | msg['Subject'] = subject 144 | msg['From'] = sender 145 | msg['To'] = recipient 146 | s = smtplib.SMTP('localhost') 147 | s.send_message(msg) 148 | s.quit() 149 | 150 | def is_authorized(owner, session): 151 | if ("user" in session) and (owner == session.get("user")): 152 | return True 153 | elif session.get("admin"): 154 | return True 155 | else: 156 | return False 157 | -------------------------------------------------------------------------------- /ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /ffmap/web/static/css/datatables/dataTables.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable{border-collapse:separate !important}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} 2 | -------------------------------------------------------------------------------- /ffmap/web/static/css/style.css: -------------------------------------------------------------------------------- 1 | #map { 2 | cursor: default; 3 | } 4 | .leaflet-control label { 5 | font-weight: normal; 6 | } 7 | .leaflet-dragging #map { 8 | cursor: grabbing; 9 | } 10 | 11 | .popup-headline { 12 | font-size: 150%; 13 | } 14 | .popup-headline.with-neighbours { 15 | border-bottom: 1px solid lightgray; 16 | } 17 | .popup-latlng { 18 | font-size:14px; 19 | } 20 | table.neighbours td { 21 | padding: 0 3px; 22 | } 23 | table.neighbours { 24 | border-collapse: separate; 25 | border-spacing: 2px; 26 | } 27 | 28 | .panel-heading { 29 | padding: 6px 12px; 30 | } 31 | 32 | .graph { 33 | height: 200px; 34 | width: 100%; 35 | } 36 | .graph .legendLabel { 37 | padding-left: 5px; 38 | padding-right: 7px; 39 | } 40 | .graph-pie { 41 | height: 250px; 42 | width: 100%; 43 | } 44 | .graph-pie .legendLabel { 45 | padding-left: 5px; 46 | padding-right: 7px; 47 | } 48 | 49 | .hoodv2 { 50 | color: #2db200; 51 | } 52 | .hoodlocal { 53 | color: #ffbf00; 54 | } 55 | 56 | .hoodv2 a { 57 | color: #2db200; 58 | } 59 | .hoodlocal a { 60 | color: #ffbf00; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /ffmap/web/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/img/favicon.ico -------------------------------------------------------------------------------- /ffmap/web/static/img/freifunk.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <svg 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" 4 | xmlns:cc="http://creativecommons.org/ns#" 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 6 | xmlns:svg="http://www.w3.org/2000/svg" 7 | xmlns="http://www.w3.org/2000/svg" 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 10 | version="1.2" 11 | width="115" 12 | height="85" 13 | viewBox="0 0 170 165" 14 | preserveAspectRatio="true" 15 | id="svg2" 16 | inkscape:version="0.91 r13725" 17 | sodipodi:docname="Freifunk-logo.svg"> 18 | <metadata 19 | id="metadata50"> 20 | <rdf:RDF> 21 | <cc:Work 22 | rdf:about=""> 23 | <dc:format>image/svg+xml</dc:format> 24 | <dc:type 25 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 26 | </cc:Work> 27 | </rdf:RDF> 28 | </metadata> 29 | <defs 30 | id="defs48" /> 31 | <sodipodi:namedview 32 | pagecolor="#ffffff" 33 | bordercolor="#666666" 34 | borderopacity="1" 35 | objecttolerance="10" 36 | gridtolerance="10" 37 | guidetolerance="10" 38 | inkscape:pageopacity="0" 39 | inkscape:pageshadow="2" 40 | inkscape:window-width="1436" 41 | inkscape:window-height="877" 42 | id="namedview46" 43 | showgrid="false" 44 | inkscape:zoom="2.8606061" 45 | inkscape:cx="53.549928" 46 | inkscape:cy="98.640182" 47 | inkscape:window-x="0" 48 | inkscape:window-y="19" 49 | inkscape:window-maximized="1" 50 | inkscape:current-layer="svg2" /> 51 | <!-- Designed by Monic Meisel and hand-coded as SVG by Alina Friedrichsen --> 52 | <g 53 | id="g8" 54 | transform="matrix(1.5737465,0,0,1.5737465,-52.395996,-15.487771)"> 55 | <circle 56 | cx="36" 57 | cy="54" 58 | r="22" 59 | transform="matrix(1.477616,0,0,1.5065066,-3.122124,-2.4350016)" 60 | stroke-miterlimit="4" 61 | id="circle10" 62 | style="fill:none;stroke:#de2c68;stroke-width:1.14544892;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> 63 | <circle 64 | cx="74" 65 | cy="44" 66 | r="22" 67 | transform="matrix(1.5950476,0,0,1.6223254,-10.072136,-9.1778182)" 68 | stroke-miterlimit="4" 69 | id="circle12" 70 | style="fill:none;stroke:#de2c68;stroke-width:1.06239557;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> 71 | <circle 72 | cx="74" 73 | cy="44" 74 | r="29" 75 | transform="matrix(1.5831269,0,0,1.5986441,-9.3397104,-8.1360194)" 76 | stroke-miterlimit="4" 77 | id="circle14" 78 | style="fill:none;stroke:#de2c68;stroke-width:5.81192684;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> 79 | <polygon 80 | points="31,44 26,44 30,48 22,48 22,52 30,52 26,56 31,56 37,50 " 81 | transform="matrix(1.4493674,0,0,1.3737988,-2.70016,2.5042672)" 82 | id="polygon16" 83 | style="fill:#ffcc33;fill-opacity:1" /> 84 | <polygon 85 | points="81,59 75,53 88,53 88,47 75,47 81,41 73,41 64,50 73,59 " 86 | transform="matrix(1.5159598,0,0,1.568926,-5.2784314,-7.233634)" 87 | id="polygon18" 88 | style="fill:#ffcc33;fill-opacity:1" /> 89 | </g> 90 | </svg> 91 | -------------------------------------------------------------------------------- /ffmap/web/static/img/offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/img/offline.png -------------------------------------------------------------------------------- /ffmap/web/static/img/online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/img/online.png -------------------------------------------------------------------------------- /ffmap/web/static/img/router.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="11.524549" 30 | inkscape:cy="7.6508884" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#2b9230;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_blue.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_blue.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="45.254834" 29 | inkscape:cx="6.1575261" 30 | inkscape:cy="6.8281194" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#123cff;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_blue_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_blue.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="45.254834" 29 | inkscape:cx="6.1575261" 30 | inkscape:cy="6.8281194" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#123cff;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_direct_green.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_direct_green.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="64" 29 | inkscape:cx="6.6494118" 30 | inkscape:cy="8.8263052" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" 41 | inkscape:object-paths="true" /> 42 | <metadata 43 | id="metadata4147"> 44 | <rdf:RDF> 45 | <cc:Work 46 | rdf:about=""> 47 | <dc:format>image/svg+xml</dc:format> 48 | <dc:type 49 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 50 | <dc:title /> 51 | </cc:Work> 52 | </rdf:RDF> 53 | </metadata> 54 | <g 55 | inkscape:label="Ebene 1" 56 | inkscape:groupmode="layer" 57 | id="layer1" 58 | transform="translate(0,-1038.3621)"> 59 | <circle 60 | style="fill:#2fa034;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 61 | id="path4690-2" 62 | r="6.6121397" 63 | cy="1045.3621" 64 | cx="7" /> 65 | <circle 66 | style="fill:#000000;fill-opacity:1" 67 | id="path4690" 68 | cx="7.0002623" 69 | cy="1045.3611" 70 | r="5.464994" /> 71 | <circle 72 | style="fill:#2fa034;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 73 | id="path4690-2-0" 74 | r="4.3582368" 75 | cy="1045.3621" 76 | cx="7" /> 77 | <circle 78 | style="fill:#000000;fill-opacity:1" 79 | id="path4690-3" 80 | cx="7" 81 | cy="1045.3621" 82 | r="3.4499073" /> 83 | <circle 84 | style="fill:#2fa034;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 85 | id="path4690-2-0-6" 86 | r="2.5210376" 87 | cy="1045.3621" 88 | cx="7" /> 89 | <circle 90 | style="fill:#000000;fill-opacity:1" 91 | id="path4690-3-2" 92 | cx="7" 93 | cy="1045.3621" 94 | r="1.6245422" /> 95 | <path 96 | style="fill:#2fa034;fill-opacity:1" 97 | id="path4134" 98 | sodipodi:type="arc" 99 | sodipodi:cx="7.0023694" 100 | sodipodi:cy="1045.3662" 101 | sodipodi:rx="5.9178114" 102 | sodipodi:ry="5.9178114" 103 | sodipodi:start="5.8553576" 104 | sodipodi:end="3.5661914" 105 | d="m 12.386804,1042.9109 a 5.9178114,5.9178114 0 0 1 -1.722167,7.1038 5.9178114,5.9178114 0 0 1 -7.3095066,0.012 5.9178114,5.9178114 0 0 1 -1.745095,-7.0982 l 5.392334,2.4379 z" /> 106 | </g> 107 | </svg> 108 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_direct_red.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_direct_green.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="64" 29 | inkscape:cx="6.6494118" 30 | inkscape:cy="8.8263052" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" 41 | inkscape:object-paths="true" /> 42 | <metadata 43 | id="metadata4147"> 44 | <rdf:RDF> 45 | <cc:Work 46 | rdf:about=""> 47 | <dc:format>image/svg+xml</dc:format> 48 | <dc:type 49 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 50 | <dc:title /> 51 | </cc:Work> 52 | </rdf:RDF> 53 | </metadata> 54 | <g 55 | inkscape:label="Ebene 1" 56 | inkscape:groupmode="layer" 57 | id="layer1" 58 | transform="translate(0,-1038.3621)"> 59 | <circle 60 | style="fill:#dd0c0c;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 61 | id="path4690-2" 62 | r="6.6121397" 63 | cy="1045.3621" 64 | cx="7" /> 65 | <circle 66 | style="fill:#000000;fill-opacity:1" 67 | id="path4690" 68 | cx="7.0002623" 69 | cy="1045.3611" 70 | r="5.464994" /> 71 | <circle 72 | style="fill:#dd0c0c;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 73 | id="path4690-2-0" 74 | r="4.3582368" 75 | cy="1045.3621" 76 | cx="7" /> 77 | <circle 78 | style="fill:#000000;fill-opacity:1" 79 | id="path4690-3" 80 | cx="7" 81 | cy="1045.3621" 82 | r="3.4499073" /> 83 | <circle 84 | style="fill:#dd0c0c;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 85 | id="path4690-2-0-6" 86 | r="2.5210376" 87 | cy="1045.3621" 88 | cx="7" /> 89 | <circle 90 | style="fill:#000000;fill-opacity:1" 91 | id="path4690-3-2" 92 | cx="7" 93 | cy="1045.3621" 94 | r="1.6245422" /> 95 | <path 96 | style="fill:#dd0c0c;fill-opacity:1" 97 | id="path4134" 98 | sodipodi:type="arc" 99 | sodipodi:cx="7.0023694" 100 | sodipodi:cy="1045.3662" 101 | sodipodi:rx="5.9178114" 102 | sodipodi:ry="5.9178114" 103 | sodipodi:start="5.8553576" 104 | sodipodi:end="3.5661914" 105 | d="m 12.386804,1042.9109 a 5.9178114,5.9178114 0 0 1 -1.722167,7.1038 5.9178114,5.9178114 0 0 1 -7.3095066,0.012 5.9178114,5.9178114 0 0 1 -1.745095,-7.0982 l 5.392334,2.4379 z" /> 106 | </g> 107 | </svg> 108 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_direct_yellow.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_direct_green.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="64" 29 | inkscape:cx="6.6494118" 30 | inkscape:cy="8.8263052" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" 41 | inkscape:object-paths="true" /> 42 | <metadata 43 | id="metadata4147"> 44 | <rdf:RDF> 45 | <cc:Work 46 | rdf:about=""> 47 | <dc:format>image/svg+xml</dc:format> 48 | <dc:type 49 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 50 | <dc:title /> 51 | </cc:Work> 52 | </rdf:RDF> 53 | </metadata> 54 | <g 55 | inkscape:label="Ebene 1" 56 | inkscape:groupmode="layer" 57 | id="layer1" 58 | transform="translate(0,-1038.3621)"> 59 | <circle 60 | style="fill:#ffea12;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 61 | id="path4690-2" 62 | r="6.6121397" 63 | cy="1045.3621" 64 | cx="7" /> 65 | <circle 66 | style="fill:#000000;fill-opacity:1" 67 | id="path4690" 68 | cx="7.0002623" 69 | cy="1045.3611" 70 | r="5.464994" /> 71 | <circle 72 | style="fill:#ffea12;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 73 | id="path4690-2-0" 74 | r="4.3582368" 75 | cy="1045.3621" 76 | cx="7" /> 77 | <circle 78 | style="fill:#000000;fill-opacity:1" 79 | id="path4690-3" 80 | cx="7" 81 | cy="1045.3621" 82 | r="3.4499073" /> 83 | <circle 84 | style="fill:#ffea12;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 85 | id="path4690-2-0-6" 86 | r="2.5210376" 87 | cy="1045.3621" 88 | cx="7" /> 89 | <circle 90 | style="fill:#000000;fill-opacity:1" 91 | id="path4690-3-2" 92 | cx="7" 93 | cy="1045.3621" 94 | r="1.6245422" /> 95 | <path 96 | style="fill:#ffea12;fill-opacity:1" 97 | id="path4134" 98 | sodipodi:type="arc" 99 | sodipodi:cx="7.0023694" 100 | sodipodi:cy="1045.3662" 101 | sodipodi:rx="5.9178114" 102 | sodipodi:ry="5.9178114" 103 | sodipodi:start="5.8553576" 104 | sodipodi:end="3.5661914" 105 | d="m 12.386804,1042.9109 a 5.9178114,5.9178114 0 0 1 -1.722167,7.1038 5.9178114,5.9178114 0 0 1 -7.3095066,0.012 5.9178114,5.9178114 0 0 1 -1.745095,-7.0982 l 5.392334,2.4379 z" /> 106 | </g> 107 | </svg> 108 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_green.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_green.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#0f7014;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_green_v2.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_green_v2.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#2fbb34;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_green_v2_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_green_v2.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#2fbb34;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_green_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_green.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#0f7014;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_grey.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_grey.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#999999;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_grey_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_grey.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title /> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#999999;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_red.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_red.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="11.524549" 30 | inkscape:cy="7.6508884" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#cc0c0c;fill-opacity:0.99607843" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_red_v2.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_red_v2.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="11.524549" 30 | inkscape:cy="7.6508884" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#ff6666;fill-opacity:0.99607843" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_red_v2_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_red_v2.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="11.524549" 30 | inkscape:cy="7.6508884" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#ff6666;fill-opacity:0.99607843" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_red_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_red.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="11.524549" 30 | inkscape:cy="7.6508884" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#cc0c0c;fill-opacity:0.99607843" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_yellow.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_yellow.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#ffea12;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#000000;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/router_yellow_white.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 | 4 | <svg 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" 6 | xmlns:cc="http://creativecommons.org/ns#" 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 | xmlns:svg="http://www.w3.org/2000/svg" 9 | xmlns="http://www.w3.org/2000/svg" 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 12 | width="14" 13 | height="14" 14 | viewBox="0 0 14 14" 15 | id="svg4142" 16 | version="1.1" 17 | inkscape:version="0.91 r13725" 18 | sodipodi:docname="router_yellow.svg"> 19 | <defs 20 | id="defs4144" /> 21 | <sodipodi:namedview 22 | id="base" 23 | pagecolor="#ffffff" 24 | bordercolor="#666666" 25 | borderopacity="1.0" 26 | inkscape:pageopacity="0.0" 27 | inkscape:pageshadow="2" 28 | inkscape:zoom="22.627417" 29 | inkscape:cx="3.7021802" 30 | inkscape:cy="7.5625001" 31 | inkscape:document-units="px" 32 | inkscape:current-layer="layer1" 33 | showgrid="false" 34 | units="px" 35 | inkscape:window-width="1436" 36 | inkscape:window-height="858" 37 | inkscape:window-x="0" 38 | inkscape:window-y="19" 39 | inkscape:window-maximized="1" 40 | width="14in" /> 41 | <metadata 42 | id="metadata4147"> 43 | <rdf:RDF> 44 | <cc:Work 45 | rdf:about=""> 46 | <dc:format>image/svg+xml</dc:format> 47 | <dc:type 48 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 49 | <dc:title></dc:title> 50 | </cc:Work> 51 | </rdf:RDF> 52 | </metadata> 53 | <g 54 | inkscape:label="Ebene 1" 55 | inkscape:groupmode="layer" 56 | id="layer1" 57 | transform="translate(0,-1038.3621)"> 58 | <circle 59 | style="fill:#ffea12;fill-opacity:1" 60 | id="path4690" 61 | cx="7" 62 | cy="1045.3621" 63 | r="6.6121397" /> 64 | <circle 65 | style="fill:#ffffff;fill-opacity:1" 66 | id="path4134" 67 | cx="7" 68 | cy="1045.3621" 69 | r="2.0780513" /> 70 | </g> 71 | </svg> 72 | -------------------------------------------------------------------------------- /ffmap/web/static/img/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/img/unknown.png -------------------------------------------------------------------------------- /ffmap/web/static/js/datatables/dataTables.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 3 integration 3 | ©2011-2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,e){a||(a=window);if(!e||!e.fn.dataTable)e=require("datatables.net")(a,e).$;return b(e,a,a.document)}:b(jQuery,window,document)})(function(b,a,e){var d=b.fn.dataTable;b.extend(!0,d.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(d.ext.classes, 6 | {sWrapper:"dataTables_wrapper form-inline dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});d.ext.renderer.pageButton.bootstrap=function(a,h,r,m,j,n){var o=new d.Api(a),s=a.oClasses,k=a.oLanguage.oPaginate,t=a.oLanguage.oAria.paginate||{},f,g,p=0,q=function(d,e){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; 7 | l=0;for(h=e.length;l<h;l++)if(c=e[l],b.isArray(c))q(d,c);else{g=f="";switch(c){case "ellipsis":f="…";g="disabled";break;case "first":f=k.sFirst;g=c+(0<j?"":" disabled");break;case "previous":f=k.sPrevious;g=c+(0<j?"":" disabled");break;case "next":f=k.sNext;g=c+(j<n-1?"":" disabled");break;case "last":f=k.sLast;g=c+(j<n-1?"":" disabled");break;default:f=c+1,g=j===c?"active":""}f&&(i=b("<li>",{"class":s.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("<a>",{href:"#", 8 | "aria-controls":a.sTableId,"aria-label":t[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(f)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(e.activeElement).data("dt-idx")}catch(u){}q(b(h).empty().html('<ul class="pagination"/>').children("ul"),m);i&&b(h).find("[data-dt-idx="+i+"]").focus()};d.TableTools&&(b.extend(!0,d.TableTools.classes,{container:"DTTT btn-group",buttons:{normal:"btn btn-default",disabled:"disabled"},collection:{container:"DTTT_dropdown dropdown-menu", 9 | buttons:{normal:"",disabled:"disabled"}},print:{info:"DTTT_print_info"},select:{row:"active"}}),b.extend(!0,d.TableTools.DEFAULTS.oTags,{collection:{container:"ul",button:"li",liner:"a"}}));return d}); 10 | -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/jquery.flot.byte.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; 3 | 4 | var options = {}; 5 | 6 | //Round to nearby lower multiple of base 7 | function floorInBase(n, base) { 8 | return base * Math.floor(n / base); 9 | } 10 | 11 | function init(plot) { 12 | plot.hooks.processDatapoints.push(function (plot) { 13 | $.each(plot.getAxes(), function(axisName, axis) { 14 | var opts = axis.options; 15 | if (opts.mode === "byte" || opts.mode === "byteRate") { 16 | axis.tickGenerator = function (axis) { 17 | var returnTicks = [], 18 | tickSize = 2, 19 | delta = axis.delta, 20 | steps = 0, 21 | tickMin = 0, 22 | tickVal, 23 | tickCount = 0; 24 | 25 | //Set the reference for the formatter 26 | if (opts.mode === "byteRate") { 27 | axis.rate = true; 28 | } 29 | 30 | //Enforce maximum tick Decimals 31 | if (typeof opts.tickDecimals === "number") { 32 | axis.tickDecimals = opts.tickDecimals; 33 | } else { 34 | axis.tickDecimals = 2; 35 | } 36 | 37 | //Count the steps 38 | while (Math.abs(delta) >= 1024) { 39 | steps++; 40 | delta /= 1024; 41 | } 42 | 43 | //Set the tick size relative to the remaining delta 44 | while (tickSize <= 1024) { 45 | if (delta <= tickSize) { 46 | break; 47 | } 48 | tickSize *= 2; 49 | } 50 | 51 | //Tell flot the tickSize we've calculated 52 | if (typeof opts.minTickSize !== "undefined" && tickSize < opts.minTickSize) { 53 | axis.tickSize = opts.minTickSize; 54 | } else { 55 | axis.tickSize = tickSize * Math.pow(1024,steps); 56 | } 57 | 58 | //Calculate the new ticks 59 | tickMin = floorInBase(axis.min, axis.tickSize); 60 | do { 61 | tickVal = tickMin + (tickCount++) * axis.tickSize; 62 | returnTicks.push(tickVal); 63 | } while (tickVal < axis.max); 64 | 65 | return returnTicks; 66 | }; 67 | 68 | axis.tickFormatter = function(size, axis) { 69 | var ext, steps = 0; 70 | 71 | while (Math.abs(size) >= 1024) { 72 | steps++; 73 | size /= 1024; 74 | } 75 | 76 | 77 | switch (steps) { 78 | case 0: ext = " B"; break; 79 | case 1: ext = " KiB"; break; 80 | case 2: ext = " MiB"; break; 81 | case 3: ext = " GiB"; break; 82 | case 4: ext = " TiB"; break; 83 | case 5: ext = " PiB"; break; 84 | case 6: ext = " EiB"; break; 85 | case 7: ext = " ZiB"; break; 86 | case 8: ext = " YiB"; break; 87 | } 88 | 89 | 90 | if (typeof axis.rate !== "undefined") { 91 | ext += "/s"; 92 | } 93 | 94 | return (size.toFixed(axis.tickDecimals) + ext); 95 | }; 96 | } 97 | else if (opts.mode === "bit" || opts.mode === "bitRate") { 98 | axis.tickGenerator = function (axis) { 99 | var returnTicks = [], 100 | tickSize = 2, 101 | delta = axis.delta, 102 | steps = 0, 103 | tickMin = 0, 104 | tickVal, 105 | tickCount = 0; 106 | 107 | //Set the reference for the formatter 108 | if (opts.mode === "bitRate") { 109 | axis.rate = true; 110 | } 111 | 112 | //Enforce maximum tick Decimals 113 | if (typeof opts.tickDecimals === "number") { 114 | axis.tickDecimals = opts.tickDecimals; 115 | } else { 116 | axis.tickDecimals = 0; 117 | } 118 | 119 | //Count the steps 120 | while (Math.abs(delta) >= 1000) { 121 | steps++; 122 | delta /= 1000; 123 | } 124 | 125 | //Set the tick size relative to the remaining delta 126 | while (tickSize <= 1000) { 127 | if (delta <= tickSize) { 128 | break; 129 | } 130 | tickSize *= 2; 131 | } 132 | 133 | //Tell flot the tickSize we've calculated 134 | if (typeof opts.minTickSize !== "undefined" && tickSize < opts.minTickSize) { 135 | axis.tickSize = opts.minTickSize; 136 | } else { 137 | axis.tickSize = tickSize * Math.pow(1000,steps); 138 | } 139 | 140 | //Calculate the new ticks 141 | tickMin = floorInBase(axis.min, axis.tickSize); 142 | do { 143 | tickVal = tickMin + (tickCount++) * axis.tickSize; 144 | returnTicks.push(tickVal); 145 | } while (tickVal < axis.max); 146 | 147 | return returnTicks; 148 | }; 149 | 150 | axis.tickFormatter = function(size, axis) { 151 | var ext, steps = 0; 152 | 153 | while (Math.abs(size) >= 1000) { 154 | steps++; 155 | size /= 1000; 156 | } 157 | 158 | 159 | switch (steps) { 160 | case 0: ext = " b"; break; 161 | case 1: ext = " kb"; break; 162 | case 2: ext = " Mb"; break; 163 | case 3: ext = " Gb"; break; 164 | case 4: ext = " Tb"; break; 165 | case 5: ext = " Pb"; break; 166 | case 6: ext = " Eb"; break; 167 | case 7: ext = " Zb"; break; 168 | case 8: ext = " Yb"; break; 169 | } 170 | 171 | 172 | if (typeof axis.rate !== "undefined") { 173 | ext += "/s"; 174 | } 175 | 176 | return (size.toFixed(axis.tickDecimals) + ext); 177 | }; 178 | } 179 | }); 180 | }); 181 | } 182 | 183 | $.plot.plugins.push({ 184 | init: init, 185 | options: options, 186 | name: "byte", 187 | version: "0.1" 188 | }); 189 | })(jQuery); -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/jquery.flot.downsample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | 4 | Copyright (c) 2013 by Sveinn Steinarsson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | (function ($) { 26 | "use strict"; 27 | 28 | var floor = Math.floor, 29 | abs = Math.abs; 30 | 31 | function largestTriangleThreeBuckets(data, threshold) { 32 | 33 | var data_length = data.length; 34 | if (threshold >= data_length || threshold === 0) { 35 | return data; // Nothing to do 36 | } 37 | 38 | var sampled = [], 39 | sampled_index = 0; 40 | 41 | // Bucket size. Leave room for start and end data points 42 | var every = (data_length - 2) / (threshold - 2); 43 | 44 | var a = 0, // Initially a is the first point in the triangle 45 | max_area_point, 46 | max_area, 47 | area, 48 | next_a; 49 | 50 | sampled[ sampled_index++ ] = data[ a ]; // Always add the first point 51 | 52 | for (var i = 0; i < threshold - 2; i++) { 53 | 54 | // Calculate point average for next bucket (containing c) 55 | var avg_x = 0, 56 | avg_y = 0, 57 | avg_range_start = floor( ( i + 1 ) * every ) + 1, 58 | avg_range_end = floor( ( i + 2 ) * every ) + 1; 59 | avg_range_end = avg_range_end < data_length ? avg_range_end : data_length; 60 | 61 | var avg_range_length = avg_range_end - avg_range_start; 62 | 63 | for ( ; avg_range_start<avg_range_end; avg_range_start++ ) { 64 | avg_x += data[ avg_range_start ][ 0 ] * 1; // * 1 enforces Number (value may be Date) 65 | avg_y += data[ avg_range_start ][ 1 ] * 1; 66 | } 67 | avg_x /= avg_range_length; 68 | avg_y /= avg_range_length; 69 | 70 | // Get the range for this bucket 71 | var range_offs = floor( (i + 0) * every ) + 1, 72 | range_to = floor( (i + 1) * every ) + 1; 73 | 74 | // Point a 75 | var point_a_x = data[ a ][ 0 ] * 1, // enforce Number (value may be Date) 76 | point_a_y = data[ a ][ 1 ] * 1; 77 | 78 | max_area = area = -1; 79 | 80 | for ( ; range_offs < range_to; range_offs++ ) { 81 | // Calculate triangle area over three buckets 82 | area = abs( ( point_a_x - avg_x ) * ( data[ range_offs ][ 1 ] - point_a_y ) - 83 | ( point_a_x - data[ range_offs ][ 0 ] ) * ( avg_y - point_a_y ) 84 | ) * 0.5; 85 | if ( area > max_area ) { 86 | max_area = area; 87 | max_area_point = data[ range_offs ]; 88 | next_a = range_offs; // Next a is this b 89 | } 90 | } 91 | 92 | sampled[ sampled_index++ ] = max_area_point; // Pick this point from the bucket 93 | a = next_a; // This a is the next a (chosen b) 94 | } 95 | 96 | sampled[ sampled_index++ ] = data[ data_length - 1 ]; // Always add last 97 | 98 | return sampled; 99 | } 100 | 101 | 102 | function processRawData ( plot, series ) { 103 | series.data = largestTriangleThreeBuckets( series.data, series.downsample.threshold ); 104 | } 105 | 106 | 107 | var options = { 108 | series: { 109 | downsample: { 110 | threshold: 1000 // 0 disables downsampling for this series. 111 | } 112 | } 113 | }; 114 | 115 | function init(plot) { 116 | plot.hooks.processRawData.push(processRawData); 117 | } 118 | 119 | $.plot.plugins.push({ 120 | init: init, 121 | options: options, 122 | name: "downsample", 123 | version: "1.0" 124 | }); 125 | 126 | })(jQuery); 127 | -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/jquery.flot.hiddengraphs.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* 6 | * Plugin to hide series in flot graphs. 7 | * 8 | * To activate, set legend.hideable to true in the flot options object. 9 | * To hide one or more series by default, set legend.hidden to an array of 10 | * label strings. 11 | * 12 | * At the moment, this only works with line and point graphs. 13 | * 14 | * Example: 15 | * 16 | * var plotdata = [ 17 | * { 18 | * data: [[1, 1], [2, 1], [3, 3], [4, 2], [5, 5]], 19 | * label: "graph 1" 20 | * }, 21 | * { 22 | * data: [[1, 0], [2, 1], [3, 0], [4, 4], [5, 3]], 23 | * label: "graph 2" 24 | * } 25 | * ]; 26 | * 27 | * plot = $.plot($("#placeholder"), plotdata, { 28 | * series: { 29 | * points: { show: true }, 30 | * lines: { show: true } 31 | * }, 32 | * legend: { 33 | * hideable: true, 34 | * hidden: ["graph 1", "graph 2"] 35 | * } 36 | * }); 37 | * 38 | */ 39 | (function ($) { 40 | var options = { }; 41 | 42 | function init(plot) { 43 | var drawnOnce = false; 44 | 45 | function findPlotSeries(label) { 46 | var plotdata = plot.getData(); 47 | for (var i = 0; i < plotdata.length; i++) { 48 | if (plotdata[i].label == label) { 49 | return plotdata[i]; 50 | } 51 | } 52 | return null; 53 | } 54 | 55 | function plotLabelClicked(label, mouseOut) { 56 | var series = findPlotSeries(label); 57 | if (!series) { 58 | return; 59 | } 60 | 61 | var options = plot.getOptions(); 62 | var switchedOff = false; 63 | 64 | if (typeof series.points.oldShow === "undefined") { 65 | series.points.oldShow = false; 66 | } 67 | if (typeof series.lines.oldShow === "undefined") { 68 | series.lines.oldShow = false; 69 | } 70 | 71 | if (series.points.show && !series.points.oldShow) { 72 | series.points.show = false; 73 | series.points.oldShow = true; 74 | switchedOff = true; 75 | } 76 | if (series.lines.show && !series.lines.oldShow) { 77 | series.lines.show = false; 78 | series.lines.oldShow = true; 79 | switchedOff = true; 80 | } 81 | 82 | if (switchedOff) { 83 | series.oldColor = series.color; 84 | series.color = "#fff"; 85 | setHidden(options, label, true); 86 | } else { 87 | var switchedOn = false; 88 | 89 | if (!series.points.show && series.points.oldShow) { 90 | series.points.show = true; 91 | series.points.oldShow = false; 92 | switchedOn = true; 93 | } 94 | if (!series.lines.show && series.lines.oldShow) { 95 | series.lines.show = true; 96 | series.lines.oldShow = false; 97 | switchedOn = true; 98 | } 99 | 100 | if (switchedOn) { 101 | series.color = series.oldColor; 102 | setHidden(options, label, false); 103 | } 104 | } 105 | } 106 | 107 | function setSetupRedraw () { 108 | // HACK: Reset the data, triggering recalculation of graph bounds 109 | plot.setData(plot.getData()); 110 | 111 | plot.setupGrid(); 112 | plot.draw(); 113 | } 114 | 115 | function setHidden(options, label, hide) { 116 | // Record state to a new variable in the legend option object. 117 | if (!options.legend.hidden) { 118 | options.legend.hidden = []; 119 | } 120 | 121 | var pos = options.legend.hidden.indexOf(label); 122 | 123 | if (hide) { 124 | if (pos < 0) { 125 | options.legend.hidden.push(label); 126 | } 127 | } else { 128 | if (pos > -1) { 129 | options.legend.hidden.splice(pos, 1); 130 | } 131 | } 132 | } 133 | 134 | function setHideAction(elem) { 135 | elem.mouseenter(function() { $(this).css("cursor", "pointer"); }) 136 | .mouseleave(function() { $(this).css("cursor", "default"); }) 137 | .unbind("click").click(function() { 138 | if ($(this).is(".legendColorBox")) { 139 | plotLabelClicked($(this).next('.legendLabel').text()); 140 | } else { 141 | plotLabelClicked($(this).parent().text()); 142 | } 143 | setSetupRedraw(); 144 | }); 145 | } 146 | 147 | function plotLabelHandlers(plot) { 148 | var options = plot.getOptions(); 149 | 150 | if (!options.legend.hideable) { 151 | return; 152 | } 153 | 154 | var p = plot.getPlaceholder(); 155 | 156 | setHideAction(p.find(".graphlabel")); 157 | setHideAction(p.find(".legendColorBox")); 158 | 159 | if (!drawnOnce) { 160 | drawnOnce = true; 161 | if (options.legend.hidden) { 162 | for (var i = 0; i < options.legend.hidden.length; i++) { 163 | plotLabelClicked(options.legend.hidden[i], true); 164 | } 165 | setSetupRedraw(); 166 | } 167 | } 168 | } 169 | 170 | function checkOptions(plot, options) { 171 | if (!options.legend.hideable) { 172 | return; 173 | } 174 | 175 | options.legend.labelFormatter = function(label, series) { 176 | return '<span class="graphlabel">' + label + '</span>'; 177 | }; 178 | } 179 | 180 | function hideDatapointsIfNecessary(plot, s, datapoints) { 181 | var options = plot.getOptions(); 182 | 183 | if (!options.legend.hideable) { 184 | return; 185 | } 186 | 187 | if (options.legend.hidden && 188 | options.legend.hidden.indexOf(s.label) > -1) { 189 | var off = false; 190 | 191 | if (s.points.show) { 192 | s.points.show = false; 193 | s.points.oldShow = true; 194 | off = true; 195 | } 196 | if (s.lines.show) { 197 | s.lines.show = false; 198 | s.lines.oldShow = true; 199 | off = true; 200 | } 201 | 202 | if (off) { 203 | s.oldColor = s.color; 204 | s.color = "#fff"; 205 | } 206 | } 207 | 208 | if (!s.points.show && !s.lines.show) { 209 | s.datapoints.format = [ null, null ]; 210 | } 211 | } 212 | 213 | plot.hooks.processOptions.push(checkOptions); 214 | 215 | plot.hooks.draw.push(function (plot, ctx) { 216 | plotLabelHandlers(plot); 217 | }); 218 | 219 | plot.hooks.processDatapoints.push(hideDatapointsIfNecessary); 220 | } 221 | 222 | $.plot.plugins.push({ 223 | init: init, 224 | options: options, 225 | name: 'hiddenGraphs', 226 | version: '1.1' 227 | }); 228 | 229 | })(jQuery); 230 | -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/jquery.flot.resize.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | It works by listening for changes on the placeholder div (through the jQuery 7 | resize event plugin) - if the size changes, it will redraw the plot. 8 | 9 | There are no options. If you need to disable the plugin for some plots, you 10 | can just fix the size of their placeholders. 11 | 12 | */ 13 | 14 | /* Inline dependency: 15 | * jQuery resize event - v1.1 - 3/14/2010 16 | * http://benalman.com/projects/jquery-resize-plugin/ 17 | * 18 | * Copyright (c) 2010 "Cowboy" Ben Alman 19 | * Dual licensed under the MIT and GPL licenses. 20 | * http://benalman.com/about/license/ 21 | */ 22 | (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); 23 | 24 | (function ($) { 25 | var options = { }; // no options 26 | 27 | function init(plot) { 28 | function onResize() { 29 | var placeholder = plot.getPlaceholder(); 30 | 31 | // somebody might have hidden us and we can't plot 32 | // when we don't have the dimensions 33 | if (placeholder.width() == 0 || placeholder.height() == 0) 34 | return; 35 | 36 | plot.resize(); 37 | plot.setupGrid(); 38 | plot.draw(); 39 | } 40 | 41 | function bindEvents(plot, eventHolder) { 42 | plot.getPlaceholder().resize(onResize); 43 | } 44 | 45 | function shutdown(plot, eventHolder) { 46 | plot.getPlaceholder().unbind("resize", onResize); 47 | } 48 | 49 | plot.hooks.bindEvents.push(bindEvents); 50 | plot.hooks.shutdown.push(shutdown); 51 | } 52 | 53 | $.plot.plugins.push({ 54 | init: init, 55 | options: options, 56 | name: 'resize', 57 | version: '1.0' 58 | }); 59 | })(jQuery); 60 | -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/jquery.flot.tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.flot.tooltip 3 | * 4 | * description: easy-to-use tooltips for Flot charts 5 | * version: 0.8.5 6 | * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround 7 | * website: https://github.com/krzysu/flot.tooltip 8 | * 9 | * build on 2015-05-11 10 | * released under MIT License, 2012 11 | */ 12 | !function(a){var b={tooltip:{show:!1,cssClass:"flotTip",content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,lines:!1,onHover:function(a,b){},$compat:!1}};b.tooltipOpts=b.tooltip;var c=function(a){this.tipPosition={x:0,y:0},this.init(a)};c.prototype.init=function(b){function c(a){var c={};c.x=a.pageX,c.y=a.pageY,b.setTooltipPosition(c)}function d(c,d,f){var g=function(a,b,c,d){return Math.sqrt((c-a)*(c-a)+(d-b)*(d-b))},h=function(a,b,c,d,e,f,h){if(!h||(h=function(a,b,c,d,e,f){if("undefined"!=typeof c)return{x:c,y:b};if("undefined"!=typeof d)return{x:a,y:d};var g,h=-1/((f-d)/(e-c));return{x:g=(e*(a*h-b+d)+c*(a*-h+b-f))/(h*(e-c)+d-f),y:h*g-h*a+b}}(a,b,c,d,e,f),h.x>=Math.min(c,e)&&h.x<=Math.max(c,e)&&h.y>=Math.min(d,f)&&h.y<=Math.max(d,f))){var i=d-f,j=e-c,k=c*f-d*e;return Math.abs(i*a+j*b+k)/Math.sqrt(i*i+j*j)}var l=g(a,b,c,d),m=g(a,b,e,f);return l>m?m:l};if(f)b.showTooltip(f,d);else if(e.plotOptions.series.lines.show&&e.tooltipOptions.lines===!0){var i=e.plotOptions.grid.mouseActiveRadius,j={distance:i+1};a.each(b.getData(),function(a,c){for(var e=0,f=-1,i=1;i<c.data.length;i++)c.data[i-1][0]<=d.x&&c.data[i][0]>=d.x&&(e=i-1,f=i);if(-1===f)return void b.hideTooltip();var k={x:c.data[e][0],y:c.data[e][1]},l={x:c.data[f][0],y:c.data[f][1]},m=h(c.xaxis.p2c(d.x),c.yaxis.p2c(d.y),c.xaxis.p2c(k.x),c.yaxis.p2c(k.y),c.xaxis.p2c(l.x),c.yaxis.p2c(l.y),!1);if(m<j.distance){var n=g(k.x,k.y,d.x,d.y)<g(d.x,d.y,l.x,l.y)?e:f,o=(c.datapoints.pointsize,[d.x,k.y+(l.y-k.y)*((d.x-k.x)/(l.x-k.x))]),p={datapoint:o,dataIndex:n,series:c,seriesIndex:a};j={distance:m,item:p}}}),j.distance<i+1?b.showTooltip(j.item,d):b.hideTooltip()}else b.hideTooltip()}var e=this,f=a.plot.plugins.length;if(this.plotPlugins=[],f)for(var g=0;f>g;g++)this.plotPlugins.push(a.plot.plugins[g].name);b.hooks.bindEvents.push(function(b,f){if(e.plotOptions=b.getOptions(),"boolean"==typeof e.plotOptions.tooltip&&(e.plotOptions.tooltipOpts.show=e.plotOptions.tooltip,e.plotOptions.tooltip=e.plotOptions.tooltipOpts,delete e.plotOptions.tooltipOpts),e.plotOptions.tooltip.show!==!1&&"undefined"!=typeof e.plotOptions.tooltip.show){e.tooltipOptions=e.plotOptions.tooltip,e.tooltipOptions.$compat?(e.wfunc="width",e.hfunc="height"):(e.wfunc="innerWidth",e.hfunc="innerHeight");e.getDomElement();a(b.getPlaceholder()).bind("plothover",d),a(f).bind("mousemove",c)}}),b.hooks.shutdown.push(function(b,e){a(b.getPlaceholder()).unbind("plothover",d),a(e).unbind("mousemove",c)}),b.setTooltipPosition=function(b){var c=e.getDomElement(),d=c.outerWidth()+e.tooltipOptions.shifts.x,f=c.outerHeight()+e.tooltipOptions.shifts.y;b.x-a(window).scrollLeft()>a(window)[e.wfunc]()-d&&(b.x-=d),b.y-a(window).scrollTop()>a(window)[e.hfunc]()-f&&(b.y-=f),e.tipPosition.x=b.x,e.tipPosition.y=b.y},b.showTooltip=function(a,c){var d=e.getDomElement(),f=e.stringFormat(e.tooltipOptions.content,a);""!==f&&(d.html(f),b.setTooltipPosition({x:c.pageX,y:c.pageY}),d.css({left:e.tipPosition.x+e.tooltipOptions.shifts.x,top:e.tipPosition.y+e.tooltipOptions.shifts.y}).show(),"function"==typeof e.tooltipOptions.onHover&&e.tooltipOptions.onHover(a,d))},b.hideTooltip=function(){e.getDomElement().hide().html("")}},c.prototype.getDomElement=function(){var b=a("."+this.tooltipOptions.cssClass);return 0===b.length&&(b=a("<div />").addClass(this.tooltipOptions.cssClass),b.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&b.css({background:"#fff","z-index":"1040",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),b},c.prototype.stringFormat=function(a,b){var c,d,e,f,g=/%p\.{0,1}(\d{0,})/,h=/%s/,i=/%c/,j=/%lx/,k=/%ly/,l=/%x\.{0,1}(\d{0,})/,m=/%y\.{0,1}(\d{0,})/,n="%x",o="%y",p="%ct";if("undefined"!=typeof b.series.threshold?(c=b.datapoint[0],d=b.datapoint[1],e=b.datapoint[2]):"undefined"!=typeof b.series.lines&&b.series.lines.steps?(c=b.series.datapoints.points[2*b.dataIndex],d=b.series.datapoints.points[2*b.dataIndex+1],e=""):(c=b.series.data[b.dataIndex][0],d=b.series.data[b.dataIndex][1],e=b.series.data[b.dataIndex][2]),null===b.series.label&&b.series.originSeries&&(b.series.label=b.series.originSeries.label),"function"==typeof a&&(a=a(b.series.label,c,d,b)),"boolean"==typeof a&&!a)return"";if("undefined"!=typeof b.series.percent?f=b.series.percent:"undefined"!=typeof b.series.percents&&(f=b.series.percents[b.dataIndex]),"number"==typeof f&&(a=this.adjustValPrecision(g,a,f)),a="undefined"!=typeof b.series.label?a.replace(h,b.series.label):a.replace(h,""),a="undefined"!=typeof b.series.color?a.replace(i,b.series.color):a.replace(i,""),a=this.hasAxisLabel("xaxis",b)?a.replace(j,b.series.xaxis.options.axisLabel):a.replace(j,""),a=this.hasAxisLabel("yaxis",b)?a.replace(k,b.series.yaxis.options.axisLabel):a.replace(k,""),this.isTimeMode("xaxis",b)&&this.isXDateFormat(b)&&(a=a.replace(l,this.timestampToDate(c,this.tooltipOptions.xDateFormat,b.series.xaxis.options))),this.isTimeMode("yaxis",b)&&this.isYDateFormat(b)&&(a=a.replace(m,this.timestampToDate(d,this.tooltipOptions.yDateFormat,b.series.yaxis.options))),"number"==typeof c&&(a=this.adjustValPrecision(l,a,c)),"number"==typeof d&&(a=this.adjustValPrecision(m,a,d)),"undefined"!=typeof b.series.xaxis.ticks){var q;q=this.hasRotatedXAxisTicks(b)?"rotatedTicks":"ticks";var r=b.dataIndex+b.seriesIndex;for(var s in b.series.xaxis[q])if(b.series.xaxis[q].hasOwnProperty(r)&&!this.isTimeMode("xaxis",b)){var t=this.isCategoriesMode("xaxis",b)?b.series.xaxis[q][r].label:b.series.xaxis[q][r].v;t===c&&(a=a.replace(l,b.series.xaxis[q][r].label))}}if("undefined"!=typeof b.series.yaxis.ticks)for(var s in b.series.yaxis.ticks)if(b.series.yaxis.ticks.hasOwnProperty(s)){var u=this.isCategoriesMode("yaxis",b)?b.series.yaxis.ticks[s].label:b.series.yaxis.ticks[s].v;u===d&&(a=a.replace(m,b.series.yaxis.ticks[s].label))}return"undefined"!=typeof b.series.xaxis.tickFormatter&&(a=a.replace(n,b.series.xaxis.tickFormatter(c,b.series.xaxis).replace(/\$/g,"$$"))),"undefined"!=typeof b.series.yaxis.tickFormatter&&(a=a.replace(o,b.series.yaxis.tickFormatter(d,b.series.yaxis).replace(/\$/g,"$$"))),e&&(a=a.replace(p,e)),a},c.prototype.isTimeMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"time"===b.series[a].options.mode},c.prototype.isXDateFormat=function(a){return"undefined"!=typeof this.tooltipOptions.xDateFormat&&null!==this.tooltipOptions.xDateFormat},c.prototype.isYDateFormat=function(a){return"undefined"!=typeof this.tooltipOptions.yDateFormat&&null!==this.tooltipOptions.yDateFormat},c.prototype.isCategoriesMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"categories"===b.series[a].options.mode},c.prototype.timestampToDate=function(b,c,d){var e=a.plot.dateGenerator(b,d);return a.plot.formatDate(e,c,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},c.prototype.adjustValPrecision=function(a,b,c){var d,e=b.match(a);return null!==e&&""!==RegExp.$1&&(d=RegExp.$1,c=c.toFixed(d),b=b.replace(a,c)),b},c.prototype.hasAxisLabel=function(b,c){return-1!==a.inArray(this.plotPlugins,"axisLabels")&&"undefined"!=typeof c.series[b].options.axisLabel&&c.series[b].options.axisLabel.length>0},c.prototype.hasRotatedXAxisTicks=function(b){return-1!==a.inArray(this.plotPlugins,"tickRotor")&&"undefined"!=typeof b.series.xaxis.rotatedTicks};var d=function(a){new c(a)};a.plot.plugins.push({init:d,options:b,name:"tooltip",version:"0.8.5"})}(jQuery); -------------------------------------------------------------------------------- /ffmap/web/static/js/graph/upstream.txt: -------------------------------------------------------------------------------- 1 | <script src="https://raw.githubusercontent.com/mde/timezone-js/v0.4/src/date.js"></script> 2 | <script src="https://raw.githubusercontent.com/flot/flot/v0.8.3/jquery.flot.js"></script> 3 | <script src="https://raw.githubusercontent.com/flot/flot/v0.8.3/jquery.flot.time.js"></script> 4 | <script src="https://raw.githubusercontent.com/whatbox/jquery.flot.byte/master/jquery.flot.byte.js"></script> 5 | <script src="https://raw.githubusercontent.com/flot/flot/v0.8.3/jquery.flot.selection.js"></script> 6 | <script src="https://raw.githubusercontent.com/sveinn-steinarsson/flot-downsample/master/jquery.flot.downsample.js"></script> 7 | -------------------------------------------------------------------------------- /ffmap/web/static/leaflet/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/leaflet/images/layers-2x.png -------------------------------------------------------------------------------- /ffmap/web/static/leaflet/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/leaflet/images/layers.png -------------------------------------------------------------------------------- /ffmap/web/static/leaflet/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/leaflet/images/marker-icon-2x.png -------------------------------------------------------------------------------- /ffmap/web/static/leaflet/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/leaflet/images/marker-icon.png -------------------------------------------------------------------------------- /ffmap/web/static/leaflet/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreifunkFranken/fff-monitoring/36e99bec528454a69d03ecadccbb0b56c0119c70/ffmap/web/static/leaflet/images/marker-shadow.png -------------------------------------------------------------------------------- /ffmap/web/templates/apidoc.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: API Guide{% endblock %} 3 | {% block head %}{{super()}} 4 | <style type="text/css"> 5 | .jumbotron h1 { 6 | font-size:30px; 7 | font-weight:bold; 8 | margin-top:16px; 9 | margin-bottom:40px; 10 | } 11 | .jumbotron h2 { 12 | font-size:18px; 13 | font-weight:bold; 14 | font-style:italic; 15 | margin:6px 0; 16 | } 17 | .jumbotron p { 18 | font-size:16px; 19 | margin:6px 0; 20 | } 21 | .jumbotron td { 22 | padding:10px 20px; 23 | vertical-align:top; 24 | } 25 | .jumbotron th { 26 | padding:10px 20px; 27 | font-size:18px; 28 | } 29 | .jumbotron .apilink { 30 | font-weight: bold; 31 | font-style: normal; 32 | white-space: pre; 33 | font-family: monospace; 34 | } 35 | .jumbotron .apidesc { 36 | font-weight: normal; 37 | } 38 | .jumbotron .uneven { 39 | background-color:#FFFFFF; 40 | } 41 | </style> 42 | {% endblock %} 43 | {% block content %} 44 | <div class="jumbotron"> 45 | <h1>Freifunk Franken Monitoring - API Guide</h1> 46 | <table> 47 | <tr> 48 | <th style="width:240px">Function</th> 49 | <th>Details</th> 50 | </tr> 51 | <tr class="uneven"> 52 | <td> 53 | <h2>Nearest router</h2> 54 | </td> 55 | <td> 56 | <p class="apilink">/api/get_nearest_router?lat=<latitude>&lng=<longitude></p> 57 | <p class="apidesc">Returns JSON file with basic data of router next to coordinates.</p> 58 | </td> 59 | </tr> 60 | <tr> 61 | <td> 62 | <h2>Router by mac address</h2> 63 | </td> 64 | <td> 65 | <p class="apilink">/api/get_router_by_mac/<mac_with_colon_separators></p> 66 | <p class="apidesc">Redirects to router page.</p> 67 | </td> 68 | </tr> 69 | <tr class="uneven"> 70 | <td> 71 | <h2>Nodelist based on FF Ansbach scheme</h2> 72 | </td> 73 | <td> 74 | <p class="apilink">/api/nodelist</p> 75 | <p class="apidesc">Returns JSON file of all routers based on scheme of FF Ansbach (<a href="https://github.com/ffansbach/de-map/blob/master/schema/nodelist-schema-1.0.0.json">GitHub</a>).</p> 76 | <p class="apidesc">This includes the following information: <span style="font-style:italic">Monitoring ID, hostname, Monitoring Link, on/off, clients, last contact and coordinates.</span></p> 77 | </td> 78 | </tr> 79 | <tr> 80 | <td> 81 | <h2>Routers without position</h2> 82 | </td> 83 | <td> 84 | <p class="apilink">/api/nopos</p> 85 | <p class="apidesc">Returns JSON file of all routers without coordinates set.</p> 86 | </td> 87 | </tr> 88 | <tr class="uneven"> 89 | <td> 90 | <h2>Extended router list</h2> 91 | </td> 92 | <td> 93 | <p class="apilink">/api/routers</p> 94 | <p class="apidesc">Returns JSON file of all routers with the following information: <span style="font-style:italic">Monitoring ID, hostname, MAC address, hood, status, user nickname, hardware, firmware, Monitoring link, clients, last contact, uplink interfaces and coordinates.</span></p> 95 | </td> 96 | </tr> 97 | <tr> 98 | <td> 99 | <h2>Routers of a specific user</h2> 100 | </td> 101 | <td> 102 | <p class="apilink">/api/routers_by_nickname/<user_nickname></p> 103 | <p class="apidesc">Returns JSON file of all routers belonging to the specified <user_nickname>.</p> 104 | </td> 105 | </tr> 106 | <tr class="uneven"> 107 | <td> 108 | <h2>Routers of a hood by KeyXchange ID</h2> 109 | </td> 110 | <td> 111 | <p class="apilink">/api/routers_by_keyxchange_id/<hood_keyxchange_id></p> 112 | <p class="apidesc">Returns JSON file of all routers belonging to the specified <hood_keyxchange_id>.</p> 113 | </td> 114 | </tr> 115 | <tr> 116 | <td> 117 | <h2>Wifi Analyzer node list</h2> 118 | </td> 119 | <td> 120 | <p class="apilink">/api/wifianal/<hood></p> 121 | <p class="apidesc">Returns configuration file (text/plain) for the Wifi Analyzer app (<a href="https://play.google.com/store/apps/details?id=com.farproc.wifi.analyzer&hl=en">PlayStore</a>).</p> 122 | <p class="apidesc">The file contains all routers of the selected <hood>.</p> 123 | </td> 124 | </tr> 125 | <tr class="uneven"> 126 | <td> 127 | <h2>Wifi Analyzer node list for all hoods</h2> 128 | </td> 129 | <td> 130 | <p class="apilink">/api/wifianalall</p> 131 | <p class="apidesc">Returns configuration file (text/plain) for the Wifi Analyzer app (<a href="https://play.google.com/store/apps/details?id=com.farproc.wifi.analyzer&hl=en">PlayStore</a>).</p> 132 | <p class="apidesc">The file contains all routers of all hoods.</p> 133 | </td> 134 | </tr> 135 | </table> 136 | </div> 137 | {% endblock %} 138 | -------------------------------------------------------------------------------- /ffmap/web/templates/bootstrap.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="de"> 3 | <head> 4 | {% block head %} 5 | <meta charset="utf-8"> 6 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 7 | <meta name="google" content="notranslate"> 8 | <meta name="viewport" content="{% block viewport %}width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no{% endblock %}"> 9 | 10 | <title>{% block title %}FFF Monitoring{% endblock %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endblock %} 21 | 22 | 23 | 64 | 65 |
66 | {%- block alerts %} 67 | {%- with messages = get_flashed_messages(with_categories=true) %} 68 | {%- if messages %} 69 | {%- for category, message in messages %} 70 | 74 | {%- endfor %} 75 | {%- endif %} 76 | {%- endwith %} 77 | {%- endblock %} 78 | {%- block content %}{% endblock -%} 79 |
80 | 81 | 82 | -------------------------------------------------------------------------------- /ffmap/web/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Login{% endblock %} 3 | {% block content %} 4 |
5 |
6 |

Please sign in

7 |
8 |
9 |
10 |
11 |
12 |
Login
13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 |
35 | Create Account - 36 | Reset Password 37 |
38 |
39 |
40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /ffmap/web/templates/map.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Map{% endblock %} 3 | {% block head %}{{super()}} 4 | 5 | 6 | 23 | {% endblock %} 24 | 25 | {% block content %} 26 |
27 | 33 | 34 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /ffmap/web/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Register{% endblock %} 3 | {% block content %} 4 |
5 |
6 |

Registration

7 |
8 |
9 |
10 |
11 |
12 |
Create User Account
13 |
14 |
15 |
16 | 17 |
18 | 19 |

20 | Falls bereits ein Netmon Account existiert, muss hier der gleiche Nickname 21 | verwendet werden, damit die alten Router (bis Firmware 0.5.2) dem Account 22 | zugeordnet werden können. 23 |

24 |
25 |
26 |
27 | 28 |
29 | 30 |

31 | Die selbe E-Mail Adresse muss auf den Routern (mit Firmware > 0.5.2) eingegeben werden, 32 | damit die Router dem Account zugeordnet werden können. 33 |

34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /ffmap/web/templates/resetpw.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Reset password{% endblock %} 3 | {% block content %} 4 |
5 |
6 |

Password forgotten

7 |
8 |
9 |
10 |
11 |
12 |
Reset password
13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /ffmap/web/templates/router_list.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Routers{% endblock %} 3 | {% block head %}{{super()}} 4 | 5 | 6 | 7 | 14 | {% endblock %} 15 | {%- block search %} 16 | 26 | {%- endblock %} 27 | {% block content %} 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {%- for router in routers %} 45 | 46 | 49 | 50 | {{ router.hood }} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {%- endfor %} 59 | 60 |
HostnameStatusHoodUserHardwareCreatedUptimeLast contactUsers
{{ router.hostname }} 47 | {%- if not router.lat and not router.lng %} - Reset!{%- endif %}{%- if router.blocked and not router.v2 %} - Blocked!{%- endif %} 48 | {{ router.status }}{%- if router.nickname %}{{ router.nickname }}{%- elif not router.contact %}missing{%- endif %}{{ router.hardware }}{{ router.created|utc2local|format_dt_date }}{{ router.sys_uptime|format_ts_diff }}{{ router.last_contact|utc2local|format_dt_date }}{{ router.clients }}
61 |
62 |
63 | {{ numrouters }} Router{{ "s" if (numrouters == 1) else "" }} found. 64 |
65 | 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /ffmap/web/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Users{% endblock %} 3 | {% block head %}{{super()}} 4 | 5 | 6 | 7 | 14 | {% endblock %} 15 | {%- block search %} 16 | {# FIXME: Search users #} 17 | {%- endblock %} 18 | {% block content %} 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {%- for user in users %} 34 | 35 | 36 | 37 | 38 | 45 | 46 | 47 | 48 | 49 | {%- endfor %} 50 | 51 |
NicknameE-MailAdminCreatedV2RoutersClients
{{ user.nickname }}{{ user.email|anon_email }}{{ user.admin }} 39 | {%- if user.created -%} 40 | {{ user.created|utc2local|format_dt_date }} 41 | {%- else -%} 42 | 43 | {%- endif -%} 44 | {{ users_v2.get(user.email.lower(), {})|v2userpercent }} %{{ user_routers.get(user.email.lower(), {}).get('routers', 0) }}{{ user_routers.get(user.email.lower(), {}).get('clients', 0) }}
52 |
53 |
54 | {{ users_count }} User{{ "s" if (users_count > 1) else "" }} found. 55 |
56 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /ffmap/web/templates/v2routers.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap.html" %} 2 | {% block title %}{{super()}} :: Statistics{% endblock %} 3 | {% block head %}{{super()}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 49 | {% endblock %} 50 | 51 | {% block content %} 52 |
53 |
54 |
55 |
Routers V1: {{ statsv1[-1]["online"] }} on, {{ statsv1[-1]["offline"] }} off, {{ statsv1[-1]["unknown"] }} unknown, {{ statsv1[-1]["orphaned"] }} orphaned; {{ statsv1[-1]["online"]+statsv1[-1]["offline"]+statsv1[-1]["unknown"]+statsv1[-1]["orphaned"] }} total
56 |
57 |
58 |
59 |
60 |
61 |
Clients V1: {{ statsv1[-1]["clients"] }}
62 |
63 |
64 |
65 |
66 |
67 |
Traffic V1
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
Routers V2: {{ statsv2[-1]["online"] }} on, {{ statsv2[-1]["offline"] }} off, {{ statsv2[-1]["unknown"] }} unknown, {{ statsv2[-1]["orphaned"] }} orphaned; {{ statsv2[-1]["online"]+statsv2[-1]["offline"]+statsv2[-1]["unknown"]+statsv2[-1]["orphaned"] }} total
76 |
77 |
78 |
79 |
80 |
81 |
Clients V2: {{ statsv2[-1]["clients"] }}
82 |
83 |
84 |
85 |
86 |
87 |
Traffic V2
88 |
89 |
90 |
91 |
92 |
93 |
94 | 107 | {% endblock %} 108 | 109 | -------------------------------------------------------------------------------- /gwinfo/gwinfofirmware.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Gateway data script for FFF Monitoring 4 | # Copyright Adrian Schmutzler, 2018. 5 | # License GPLv3 6 | # 7 | # designed for GATEWAY FIRMWARE 8 | # 9 | # v1.4.6 - 2018-10-17 10 | # - Fix IPv4/IPv6 sed (leading space in match pattern) 11 | # 12 | # v1.4.3 - 2018-08-28 13 | # - Added version to json 14 | # - GW-Firmware: Only append IPv4/IPv6/DHCP to bat0 15 | # 16 | # v1.4.2 - 2018-08-28 17 | # - Fixed IPv4 sed to ignore subnet mask 18 | # - Check for multiple IPv6 addresses 19 | # - GW-Firmware: Ignore wireless devices 20 | # - GW-Firmware: Use eth device from batctl if 21 | # - GW-Firmware: Use only br-mesh for batctl if 22 | # - GW-Firmware: Select fd43 address with :: 23 | # - GW-Firmware: Adjust DHCP to uci 24 | # 25 | # v1.4.1 - 2018-08-25 26 | # - Fixed greps for IPv4/IPv6/dnsmasq 27 | # 28 | # v1.4 - 2018-08-23 29 | # - Transmit internal IPv4/IPv6 30 | # - Transmit DHCP range for dnsmasq 31 | # 32 | # v1.3 - 2018-08-23 33 | # - Support multiple Monitoring URLs 34 | # - Use https by default 35 | # - Changed batctl default path 36 | # 37 | # v1.2.1 - 2018-01-12 38 | # - Added "grep fff" to support L2TP 39 | # 40 | # v1.2 - 2018-01-12 41 | # - Added batctl command and vpnif 42 | # 43 | # v1.1 - 2018-01-12 44 | # - Initial Version 45 | # 46 | 47 | # Config 48 | api_urls="https://monitoring.freifunk-franken.de/api/gwinfo" # space-separated list of addresses (api_urls="url1 url2") 49 | batctlpath=/usr/sbin/batctl 50 | hostname="$(uci -q get system.@system[0].hostname)" 51 | statslink="$(uci -q get gateway.@gateway[0].statslink)" 52 | 53 | # Code 54 | tmp=$(/bin/mktemp) 55 | echo "{\"version\":\"1.4.6\",\"hostname\":\"$hostname\",\"stats_page\":\"$statslink\",\"netifs\":[" > $tmp 56 | 57 | comma="" 58 | for netif in $(ls /sys/class/net); do 59 | if [ "$netif" = "lo" ] || echo "$netif" | grep -q "w" ; then # remove wXap, wXmesh, etc. 60 | continue 61 | fi 62 | mac="$(cat "/sys/class/net/$netif/address")" 63 | batctl="$("$batctlpath" -m "$netif" if | grep "eth" | sed -n 's/:.*//p')" 64 | 65 | ipv4="" 66 | ipv6="" 67 | dhcpstart="" 68 | dhcpend="" 69 | if [ "$netif" = "bat0" ]; then 70 | ipv4="$(ip -4 addr show dev br-mesh | grep " 10\." | sed 's/.* \(10\.[^ ]*\/[^ ]*\) .*/\1/')" 71 | ipv6="$(ip -6 addr show dev br-mesh | grep " fd43" | grep '::' | sed 's/.* \(fd43[^ ]*\) .*/\1/')" 72 | [ "$(echo "$ipv6" | wc -l)" = "1" ] || ipv6="" 73 | dhcpstart="$(uci -q get dhcp.mesh.start)" 74 | fi 75 | 76 | echo "$comma{\"mac\":\"$mac\",\"netif\":\"$netif\",\"vpnif\":\"$batctl\",\"ipv4\":\"$ipv4\",\"ipv6\":\"$ipv6\",\"dhcpstart\":\"$dhcpstart\",\"dhcpend\":\"$dhcpend\"}" >> $tmp 77 | comma="," 78 | done 79 | 80 | echo "],\"admins\":[" >> $tmp 81 | 82 | comma="" 83 | for admin in $(uci -q get gateway.@gateway[0].admin); do 84 | echo "$comma\"$admin\"" >> $tmp && comma="," 85 | done 86 | 87 | echo "]}" >> $tmp 88 | 89 | for api_url in $api_urls; do 90 | /usr/bin/curl -k -v -H "Content-type: application/json; charset=UTF-8" -X POST --data-binary @$tmp $api_url 91 | done 92 | /bin/rm "$tmp" 93 | -------------------------------------------------------------------------------- /gwinfo/sendgwinfo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Gateway data script for FFF Monitoring 4 | # Copyright Adrian Schmutzler, 2018. 5 | # License GPLv3 6 | # 7 | # designed for GATEWAY SERVER 8 | # 9 | # v1.4.6 - 2018-10-17 10 | # - Fix IPv4/IPv6 sed (leading space in match pattern) 11 | # 12 | # v1.4.5 - 2018-08-29 13 | # - Fix one bug regarding DHCP range processing 14 | # 15 | # v1.4.4 - 2018-08-29 16 | # - Fix two bugs regarding DHCP range processing 17 | # 18 | # v1.4.3 - 2018-08-28 19 | # - Added version to json 20 | # 21 | # v1.4.2 - 2018-08-28 22 | # - Fixed IPv4 sed to ignore subnet mask 23 | # - Check for multiple IPv6 addresses 24 | # - Provide experimental support for isc-dhpc-server 25 | # 26 | # v1.4.1 - 2018-08-25 27 | # - Fixed greps for IPv4/IPv6/dnsmasq 28 | # 29 | # v1.4 - 2018-08-23 30 | # - Transmit internal IPv4/IPv6 31 | # - Transmit DHCP range for dnsmasq 32 | # 33 | # v1.3 - 2018-08-23 34 | # - Support multiple Monitoring URLs 35 | # - Use https by default 36 | # - Changed batctl default path 37 | # 38 | # v1.2.1 - 2018-01-12 39 | # - Added "grep fff" to support L2TP 40 | # 41 | # v1.2 - 2018-01-12 42 | # - Added batctl command and vpnif 43 | # 44 | # v1.1 - 2018-01-12 45 | # - Initial Version 46 | # 47 | 48 | # Config 49 | api_urls="https://monitoring.freifunk-franken.de/api/gwinfo" # space-separated list of addresses (api_urls="url1 url2") 50 | batctlpath=/usr/sbin/batctl # Adjust to YOUR path! 51 | hostname="MyHost" 52 | admin1="Admin" 53 | admin2= 54 | admin3= 55 | statslink="" # Provide link to stats page (MRTG or similar) 56 | dhcp=1 # 0=disabled, 1=dnsmasq, 2=isc-dhcp-server 57 | 58 | # Code 59 | tmp=$(/bin/mktemp) 60 | echo "{\"version\":\"1.4.6\",\"hostname\":\"$hostname\",\"stats_page\":\"$statslink\",\"netifs\":[" > $tmp 61 | 62 | comma="" 63 | for netif in $(ls /sys/class/net); do 64 | if [ "$netif" = "lo" ] ; then 65 | continue 66 | fi 67 | mac="$(cat "/sys/class/net/$netif/address")" 68 | batctl="$("$batctlpath" -m "$netif" if | grep "fff" | sed -n 's/:.*//p')" 69 | 70 | ipv4="$(ip -4 addr show dev "$netif" | grep " 10\." | sed 's/.* \(10\.[^ ]*\/[^ ]*\) .*/\1/')" 71 | ipv6="$(ip -6 addr show dev "$netif" | grep " fd43" | sed 's/.* \(fd43[^ ]*\) .*/\1/')" 72 | [ "$(echo "$ipv6" | wc -l)" = "1" ] || ipv6="" 73 | 74 | dhcpstart="" 75 | dhcpend="" 76 | if [ "$dhcp" = "1" ]; then 77 | dhcpdata="$(ps ax | grep "dnsmasq" | grep "$netif " | sed 's/.*dhcp-range=\([^ ]*\) .*/\1/')" 78 | dhcpstart="$(echo "$dhcpdata" | cut -d',' -f1)" 79 | dhcpend="$(echo "$dhcpdata" | cut -d',' -f2)" 80 | elif [ "$dhcp" = "2" ]; then 81 | ipv4cut="${ipv4%/*}" 82 | if [ -n "$ipv4cut" ] && grep -q "routers $ipv4cut" /etc/dhcp/dhcpd.conf; then 83 | dhcpdata="$(sed -z 's/.*range \([^;]*\);[^}]*option routers '$ipv4cut'.*/\1/' /etc/dhcp/dhcpd.conf)" 84 | dhcpstart="$(echo "$dhcpdata" | cut -d' ' -f1)" 85 | dhcpend="$(echo "$dhcpdata" | cut -d' ' -f2)" 86 | fi 87 | fi 88 | 89 | echo "$comma{\"mac\":\"$mac\",\"netif\":\"$netif\",\"vpnif\":\"$batctl\",\"ipv4\":\"$ipv4\",\"ipv6\":\"$ipv6\",\"dhcpstart\":\"$dhcpstart\",\"dhcpend\":\"$dhcpend\"}" >> $tmp 90 | comma="," 91 | done 92 | 93 | echo "],\"admins\":[" >> $tmp 94 | 95 | comma="" 96 | [ -n "$admin1" ] && echo "\"$admin1\"" >> $tmp && comma="," 97 | [ -n "$admin2" ] && echo "$comma\"$admin2\"" >> $tmp && comma="," 98 | [ -n "$admin3" ] && echo "$comma\"$admin3\"" >> $tmp 99 | 100 | echo "]}" >> $tmp 101 | 102 | for api_url in $api_urls; do 103 | /usr/bin/curl -k -v -H "Content-type: application/json; charset=UTF-8" -X POST --data-binary @$tmp $api_url 104 | done 105 | /bin/rm "$tmp" 106 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -vp /var/lib/ffmap/csv 4 | #FIXME: create dummy csv files 5 | chown -R www-data:www-data /var/lib/ffmap 6 | 7 | mkdir -vp /usr/share/ffmap 8 | cp -v ffmap/mapnik/{hoods_v2,hoods_poly,routers,routers_v2,routers_local}.xml /usr/share/ffmap 9 | sed -i -e 's#>csv/#>/var/lib/ffmap/csv/#' /usr/share/ffmap/{hoods_v2,hoods_poly,routers,routers_v2,routers_local}.xml 10 | chown www-data:www-data /usr/share/ffmap/{hoods_v2,hoods_poly,routers,routers_v2,routers_local}.xml 11 | 12 | cp -v ffmap/mapnik/tilestache.cfg /usr/share/ffmap 13 | cp -rv ffmap/web/static /usr/share/ffmap 14 | cp -rv ffmap/web/templates /usr/share/ffmap 15 | 16 | mkdir -vp /var/cache/ffmap/tiles/ 17 | chown -R www-data:www-data /var/cache/ffmap/tiles/ 18 | 19 | cp -v ffmap/systemd/*.service /etc/systemd/system/ 20 | systemctl daemon-reload 21 | 22 | python3 setup.py install --force 23 | 24 | (cd ffmap/mapnik; python3 setup.py install) 25 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf "\nStopping ...\n\n" 4 | systemctl stop uwsgi-tiles 5 | systemctl stop uwsgi-ffmap 6 | 7 | ./install.sh 8 | 9 | printf "\nStarting ...\n\n" 10 | systemctl start uwsgi-ffmap 11 | systemctl start uwsgi-tiles 12 | 13 | printf "Done.\n\n" 14 | -------------------------------------------------------------------------------- /scripts/calcglobalstats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Execute every 5 min, 2 mins after alfred comes in (sleep 120 in cron) 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.routertools import * 10 | from ffmap.maptools import * 11 | from ffmap.mysqltools import FreifunkMySQL 12 | from ffmap.stattools import record_global_stats, record_hood_stats, record_gw_stats 13 | from ffmap.hoodtools import update_hoods_v2 14 | 15 | import time 16 | start_time = time.time() 17 | 18 | mysql = FreifunkMySQL() 19 | detect_offline_routers(mysql) 20 | detect_orphaned_routers(mysql) 21 | delete_orphaned_routers(mysql) 22 | #delete_old_stats(mysql) # Only execute once daily, takes 2 minutes 23 | update_hoods_v2(mysql) 24 | record_global_stats(mysql) 25 | record_hood_stats(mysql) 26 | record_gw_stats(mysql) 27 | update_mapnik_csv(mysql) 28 | mysql.close() 29 | 30 | print("--- %.3f seconds ---" % (time.time() - start_time)) 31 | -------------------------------------------------------------------------------- /scripts/copyusers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | import pymongo 10 | from bson.json_util import dumps as bson2json 11 | from bson.objectid import ObjectId 12 | import base64 13 | import datetime 14 | 15 | client = MongoClient(tz_aware=True, connect=False) 16 | db = client.freifunk 17 | 18 | users = db.users.find({}, {"nickname": 1, "password":1, "email": 1, "token": 1, "created": 1, "admin": 1}) 19 | 20 | mysql = FreifunkMySQL() 21 | cur = mysql.cursor() 22 | for u in users: 23 | #print(u) 24 | cur.execute(""" 25 | INSERT INTO users (nickname, password, token, email, created, admin) 26 | VALUES (%s, %s, %s, %s, %s, %s) 27 | """,(u.get("nickname"),u.get("password"),u.get("token"),u.get("email",""),u.get("created"),u.get("admin",0),)) 28 | mysql.commit() 29 | mysql.close() 30 | -------------------------------------------------------------------------------- /scripts/crontiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | command="systemctl restart uwsgi-tiles" 4 | append="2>&1 | /usr/bin/logger -t uwsgi-tiles" 5 | 6 | if crontab -l | grep -q "$command" ; then 7 | echo "Cron already set." 8 | exit 1 9 | fi 10 | 11 | # Runs at X:14 12 | (crontab -l 2>/dev/null; echo "14 * * * * $command $append") | crontab - 13 | 14 | echo "Cron set successfully." 15 | exit 0 16 | 17 | -------------------------------------------------------------------------------- /scripts/csv2users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from ffmap.mysqltools import FreifunkMySQL 8 | 9 | import pymongo 10 | from bson.json_util import dumps as bson2json 11 | from bson.objectid import ObjectId 12 | import base64 13 | import datetime 14 | 15 | import csv 16 | 17 | targetfile = "/data/fff/users.txt" 18 | 19 | mysql = FreifunkMySQL() 20 | data = [] 21 | with open(targetfile, newline='') as csvfile: 22 | spamreader = csv.reader(csvfile, delimiter=';') 23 | for row in spamreader: 24 | if row[5]=="None": 25 | row[5]=None 26 | if row[1]=="None": 27 | row[1]=None 28 | if row[1]=="None": 29 | row[1]=None 30 | if row[2]=="None": 31 | row[2]=None 32 | if row[3]=="None": 33 | row[3]=None 34 | if row[4]=="True": 35 | row[4]=1 36 | else: 37 | row[4]=0 38 | row[3] = datetime.datetime.strptime(''.join(row[3].rsplit(':', 1)),"%Y-%m-%d %H:%M:%S.%f%z").strftime('%Y-%m-%d %H:%M:%S') 39 | 40 | data.append((row[0],row[5],row[1],row[2],row[3],row[4],)) 41 | 42 | mysql.executemany(""" 43 | INSERT INTO users (nickname, password, token, email, created, admin) 44 | VALUES (%s, %s, %s, %s, %s, %s) 45 | """,data) 46 | mysql.commit() 47 | mysql.close() 48 | -------------------------------------------------------------------------------- /scripts/defragtable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Execute manually 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.misc import defrag_table, writelog 10 | from ffmap.config import CONFIG 11 | from ffmap.mysqltools import FreifunkMySQL 12 | 13 | import time 14 | start_time = time.time() 15 | 16 | mysql = FreifunkMySQL() 17 | i = 1 18 | while i < len(sys.argv): 19 | defrag_table(mysql,sys.argv[i],1) 20 | i = i + 1 21 | mysql.close() 22 | 23 | writelog(CONFIG["debug_dir"] + "/deletetime.txt", "-------") 24 | print("--- Total defrag duration: %.3f seconds ---" % (time.time() - start_time)) 25 | -------------------------------------------------------------------------------- /scripts/defragtables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Execute manually 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.misc import defrag_all 10 | from ffmap.mysqltools import FreifunkMySQL 11 | 12 | import time 13 | start_time = time.time() 14 | 15 | mysql = FreifunkMySQL() 16 | if(len(sys.argv)>1): 17 | defrag_all(mysql,sys.argv[1]) 18 | else: 19 | defrag_all(mysql,False) 20 | mysql.close() 21 | 22 | print("--- Total defrag duration: %.3f seconds ---" % (time.time() - start_time)) 23 | -------------------------------------------------------------------------------- /scripts/deletestats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Execute once daily, also 2 min after full 5 mins (so it does not coincide with alfred) 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.routertools import delete_old_stats 10 | from ffmap.mysqltools import FreifunkMySQL 11 | 12 | import time 13 | start_time = time.time() 14 | 15 | mysql = FreifunkMySQL() 16 | delete_old_stats(mysql) 17 | mysql.close() 18 | 19 | print("--- Total duration: %.3f seconds ---" % (time.time() - start_time)) 20 | -------------------------------------------------------------------------------- /scripts/deleteunlinked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Deletes unlinked rows from gw_* and router_* tables 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.routertools import delete_unlinked_routers 10 | from ffmap.gwtools import delete_unlinked_gws 11 | from ffmap.mysqltools import FreifunkMySQL 12 | 13 | import time 14 | start_time = time.time() 15 | 16 | mysql = FreifunkMySQL() 17 | delete_unlinked_routers(mysql) 18 | delete_unlinked_gws(mysql) 19 | mysql.close() 20 | 21 | print("\n--- Total duration: %.3f seconds ---\n" % (time.time() - start_time)) 22 | -------------------------------------------------------------------------------- /scripts/readpolyhoods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Execute manually 4 | 5 | import os 6 | import sys 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 8 | 9 | from ffmap.hoodtools import update_hoods_poly 10 | from ffmap.mysqltools import FreifunkMySQL 11 | 12 | mysql = FreifunkMySQL() 13 | update_hoods_poly(mysql) 14 | mysql.commit() 15 | mysql.close() 16 | -------------------------------------------------------------------------------- /scripts/setupcron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | monpath=/data/fff/fff-monitoring 4 | 5 | if crontab -l | grep -q "$monpath" ; then 6 | echo "Cron already set." 7 | exit 1 8 | fi 9 | 10 | # Runs every 5 min and waits 3 min 11 | (crontab -l 2>/dev/null; echo "3-59/5 * * * * $monpath/scripts/calcglobalstats.py 2>&1 | /usr/bin/logger -t calcglobalstats") | crontab - 12 | 13 | # Runs at 4:02 14 | (crontab -l 2>/dev/null; echo "2 4 * * * $monpath/scripts/deletestats.py 2>&1 | /usr/bin/logger -t deletestats") | crontab - 15 | 16 | echo "Cron set successfully." 17 | exit 0 18 | 19 | -------------------------------------------------------------------------------- /scripts/users2csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/' + '..')) 6 | 7 | from pymongo import MongoClient 8 | from bson.json_util import dumps as bson2json 9 | from bson.objectid import ObjectId 10 | import base64 11 | import datetime 12 | 13 | targetfile = "/data/fff/users.txt" 14 | 15 | client = MongoClient(tz_aware=True, connect=False) 16 | db = client.freifunk 17 | 18 | users = db.users.find({}, {"nickname": 1, "password":1, "email": 1, "token": 1, "created": 1, "admin": 1}) 19 | 20 | with open(targetfile, "wb") as csv: 21 | for u in users: 22 | str = "%s;%s;%s;%s;%s;%s\n" % (u.get("nickname"),u.get("token"),u.get("email",""),u.get("created"),u.get("admin",0),u.get("password")) 23 | csv.write(str.encode("UTF-8")) 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='ffmap', 7 | version='0.0.1', 8 | license='GPL', 9 | description='FF-MAP', 10 | author='Dominik Heidler', 11 | author_email='dominik@heidler.eu', 12 | url='https://github.com/FreifunkFranken/fff-monitoring', 13 | #requires=['flask', 'flup'], 14 | packages=['ffmap', 'ffmap.web'], 15 | #scripts=['bin/aurbs'], 16 | #data_files=[ 17 | # ('/etc', ['templates/aurbs.yml']), 18 | # ('/usr/share/aurbs/cfg', ['templates/gpg.conf']), 19 | # ('/usr/share/doc/aurbs', ['templates/lighttpd.conf.sample']), 20 | #], 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf "\nStarting ...\n\n" 4 | systemctl start uwsgi-ffmap 5 | systemctl start uwsgi-tiles 6 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf "\nStopping ...\n\n" 4 | systemctl stop uwsgi-tiles 5 | systemctl stop uwsgi-ffmap 6 | --------------------------------------------------------------------------------