Compare commits
	
		
			243 Commits
		
	
	
		
			ffrgb-conf
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8a756f0ecb | |||
| b1a1df1a56 | |||
| 35cbfad9b0 | |||
| 68de16e73b | |||
| 9fe1d0fab8 | |||
| 538e47ed0e | |||
| fb12482833 | |||
| f5317e4559 | |||
| 07fbfe6a63 | |||
| 0fc62b342e | |||
| b6a4d02507 | |||
| 4d046c2c8e | |||
| 
						 | 
					71da7ebd8c | ||
| 
						 | 
					50188b9b1a | ||
| 
						 | 
					d29919556d | ||
| 
						 | 
					0cd0af750d | ||
| 
						 | 
					7429c5dc58 | ||
| a37f26c256 | |||
| a7cfaa8b0a | |||
| fa9d990428 | |||
| 78ab3c0796 | |||
| e65544a31b | |||
| 6563c9073e | |||
| d1e9c75c0a | |||
| ab67d494c9 | |||
| cf3d47bdd7 | |||
| 441e1e61be | |||
| b4b9d42bf4 | |||
| a768496858 | |||
| d987c63461 | |||
| 536a6c920f | |||
| f14ba5d954 | |||
| 1347254bb2 | |||
| 614093d2af | |||
| 
						 | 
					3c0cd2a36d | ||
| 
						 | 
					4fd562b84b | ||
| 
						 | 
					e448b02bb4 | ||
| 
						 | 
					4d47587278 | ||
| 
						 | 
					1fa034e1e8 | ||
| 
						 | 
					1bcc6bfd10 | ||
| 
						 | 
					35a79ba4f6 | ||
| 
						 | 
					d28e834cd5 | ||
| 
						 | 
					f40179535f | ||
| 
						 | 
					1dfe8951ac | ||
| 
						 | 
					3a3a3e014c | ||
| 
						 | 
					e083ed13a7 | ||
| 
						 | 
					8a89c0f5db | ||
| 
						 | 
					ec0e0e5445 | ||
| 
						 | 
					157da0aeba | ||
| 
						 | 
					8959e1e7cb | ||
| 
						 | 
					ddb1482a67 | ||
| 
						 | 
					6c8c12be1e | ||
| 
						 | 
					22af6d1bce | ||
| 
						 | 
					90a19a4e74 | ||
| 
						 | 
					b6f1dd066e | ||
| 
						 | 
					d021d312d7 | ||
| 
						 | 
					98e7a55f6f | ||
| 
						 | 
					2736461604 | ||
| 
						 | 
					c0ea13a8fa | ||
| 
						 | 
					2200c4bc19 | ||
| 
						 | 
					c99e4d8c42 | ||
| 
						 | 
					aeb849aca8 | ||
| 
						 | 
					3dc4d10082 | ||
| 
						 | 
					97cda8cb18 | ||
| 
						 | 
					5ad56139ac | ||
| 
						 | 
					c45f367fcd | ||
| 
						 | 
					29ea71a4d0 | ||
| 
						 | 
					d2033af7d7 | ||
| 
						 | 
					69e37f779f | ||
| 
						 | 
					a34812f2a9 | ||
| 
						 | 
					c02cf3be95 | ||
| 
						 | 
					84085727a6 | ||
| 
						 | 
					8fdc8dcabb | ||
| 
						 | 
					77c94a1f2e | ||
| 
						 | 
					2604b2b731 | ||
| 
						 | 
					31d0209cc2 | ||
| 
						 | 
					bc82e07354 | ||
| 
						 | 
					fd9eab724b | ||
| 
						 | 
					5d1d72d95d | ||
| 
						 | 
					8bf3498744 | ||
| 
						 | 
					2192500d05 | ||
| 
						 | 
					712c3f21ce | ||
| 
						 | 
					3d113c6247 | ||
| 
						 | 
					9c596531e2 | ||
| 
						 | 
					e1aa152055 | ||
| 
						 | 
					d29bb31311 | ||
| 
						 | 
					2c6303d820 | ||
| 
						 | 
					2a7e1cdaa6 | ||
| 
						 | 
					3311e70296 | ||
| 
						 | 
					e1d3a3d7b2 | ||
| 
						 | 
					25212adb81 | ||
| 
						 | 
					4fd4e27a8b | ||
| 
						 | 
					bfb1111744 | ||
| 
						 | 
					81a26b5560 | ||
| 
						 | 
					02e02f9219 | ||
| 
						 | 
					20a8c4583a | ||
| 
						 | 
					51be472ce4 | ||
| 
						 | 
					e75d865fbf | ||
| 
						 | 
					4f078f4b56 | ||
| 
						 | 
					15f8f4bd74 | ||
| 
						 | 
					3c72e12c3a | ||
| 
						 | 
					28df47c15f | ||
| 
						 | 
					ded730f36c | ||
| 
						 | 
					ad4ee19344 | ||
| 
						 | 
					48aa3a486b | ||
| 
						 | 
					ad89c910ab | ||
| 
						 | 
					7aab2f1462 | ||
| 
						 | 
					61b9e9d351 | ||
| 
						 | 
					3085c14c3b | ||
| 
						 | 
					100cf626a5 | ||
| 
						 | 
					6d57ad35c3 | ||
| 
						 | 
					0c03efbfff | ||
| 
						 | 
					ef9b9c3c3f | ||
| 
						 | 
					1b89f86d74 | ||
| 
						 | 
					b294a5d405 | ||
| 
						 | 
					957cc892a9 | ||
| 
						 | 
					6c0e667259 | ||
| 
						 | 
					13f84d5dbc | ||
| 
						 | 
					7bd2883732 | ||
| 
						 | 
					5343b5dfaf | ||
| 
						 | 
					82c9309a1d | ||
| 
						 | 
					3714795e2b | ||
| 
						 | 
					300a73213b | ||
| 
						 | 
					4dd8dc9c45 | ||
| 
						 | 
					87eb98f542 | ||
| 
						 | 
					8e8cdd63af | ||
| 
						 | 
					75c650b107 | ||
| 
						 | 
					f115600e5b | ||
| 
						 | 
					c32e951cf1 | ||
| 
						 | 
					4bc2256f2f | ||
| 
						 | 
					76fb09326e | ||
| 
						 | 
					9212b0e427 | ||
| 
						 | 
					fc596ee45f | ||
| 
						 | 
					380b13d04b | ||
| 
						 | 
					66127de355 | ||
| 
						 | 
					bd670c85fd | ||
| 
						 | 
					3c33dd63ac | ||
| 
						 | 
					dc81543bc2 | ||
| 
						 | 
					5c6f478fe6 | ||
| 
						 | 
					198b97ad18 | ||
| 
						 | 
					0aa3473a62 | ||
| 
						 | 
					e0cef88beb | ||
| 
						 | 
					71ab6e65e4 | ||
| 
						 | 
					1673fe1248 | ||
| 
						 | 
					c3cda56fe9 | ||
| 
						 | 
					bac2977b3d | ||
| 
						 | 
					b48b4f41f0 | ||
| 
						 | 
					201c74d29b | ||
| 
						 | 
					5702b5f21b | ||
| 
						 | 
					b1a5e472e4 | ||
| 
						 | 
					f8bf473666 | ||
| 
						 | 
					8da8154edd | ||
| 
						 | 
					c9dd968c92 | ||
| 
						 | 
					ed80c7657e | ||
| 
						 | 
					f729349a1f | ||
| 
						 | 
					a30d12312a | ||
| 
						 | 
					7fa0c5e522 | ||
| 
						 | 
					6af7ba6796 | ||
| 
						 | 
					bdac3c01e2 | ||
| 
						 | 
					a71eff2d39 | ||
| 
						 | 
					ad02f7789d | ||
| 
						 | 
					0e20617435 | ||
| 
						 | 
					2aadc39022 | ||
| 
						 | 
					87e4c1d2f9 | ||
| 
						 | 
					e99c38970a | ||
| 
						 | 
					0c9860192b | ||
| 
						 | 
					7bfbb1b909 | ||
| 
						 | 
					72674fd6da | ||
| 
						 | 
					338c90a2d1 | ||
| 
						 | 
					844bf99641 | ||
| 
						 | 
					dabfbfba83 | ||
| 
						 | 
					1c14ec79ab | ||
| 
						 | 
					18a2a17d97 | ||
| 
						 | 
					d0b6031d5f | ||
| 
						 | 
					2c7500f1bb | ||
| 
						 | 
					cc18e53430 | ||
| 
						 | 
					6add4f0916 | ||
| 
						 | 
					b89e99b79e | ||
| 
						 | 
					fd6c7c7f1e | ||
| 
						 | 
					6091a8b82c | ||
| 
						 | 
					c407f2e334 | ||
| 
						 | 
					9a4836257f | ||
| 
						 | 
					4315f18efe | ||
| 
						 | 
					ed93a202d6 | ||
| 
						 | 
					aa89f06342 | ||
| 
						 | 
					1887a3270c | ||
| 
						 | 
					9f95fa9c95 | ||
| 
						 | 
					1ec81fd45c | ||
| 
						 | 
					31e8667658 | ||
| 
						 | 
					4caf38e990 | ||
| 
						 | 
					a0378348b5 | ||
| 
						 | 
					519f37cd14 | ||
| 
						 | 
					1909eb291e | ||
| 
						 | 
					8d6d508bba | ||
| 
						 | 
					da029735cf | ||
| 
						 | 
					f060884b04 | ||
| 
						 | 
					57ee21f8ec | ||
| 
						 | 
					af589ee227 | ||
| 
						 | 
					d6b84eba22 | ||
| 
						 | 
					fb857717fd | ||
| 
						 | 
					b4bd941197 | ||
| 
						 | 
					13eacf5fa8 | ||
| 
						 | 
					e0630808e3 | ||
| 
						 | 
					35fd75e4f6 | ||
| 
						 | 
					914f6a344b | ||
| 
						 | 
					10ab1ead2c | ||
| 
						 | 
					b3ef2460b3 | ||
| 
						 | 
					a9630ffa78 | ||
| 
						 | 
					2a3dbc9842 | ||
| 
						 | 
					4787aa7f62 | ||
| 
						 | 
					77ac4ca3f5 | ||
| 
						 | 
					db16ea8375 | ||
| 
						 | 
					7c8456b18a | ||
| 
						 | 
					24abeb74bd | ||
| 
						 | 
					ec4732610b | ||
| 
						 | 
					cb0aa1317c | ||
| 
						 | 
					f0789392b5 | ||
| 
						 | 
					93fb72788a | ||
| 
						 | 
					def55bad15 | ||
| 
						 | 
					e2a6200d75 | ||
| 
						 | 
					a5d4140bda | ||
| 
						 | 
					9aff29a634 | ||
| 
						 | 
					f9892e5401 | ||
| 
						 | 
					329a0c44fc | ||
| 
						 | 
					779fa0a630 | ||
| 
						 | 
					c24cf3cfb5 | ||
| 
						 | 
					375627ab00 | ||
| 
						 | 
					1995855693 | ||
| 
						 | 
					1bd72654b8 | ||
| 
						 | 
					ea057cf90f | ||
| 
						 | 
					0cbf619c13 | ||
| 
						 | 
					94b4a10bd4 | ||
| 
						 | 
					b37ac2ddce | ||
| 
						 | 
					717ba27992 | ||
| 
						 | 
					374e73998a | ||
| 
						 | 
					9c3f57dd3f | ||
| 
						 | 
					6b67e4f714 | ||
| 
						 | 
					2fba8c1ac9 | ||
| 
						 | 
					8df5c2ce80 | ||
| 
						 | 
					3e63b6432e | ||
| 
						 | 
					0987b4b39a | ||
| 
						 | 
					d0c5fb2dab | ||
| 
						 | 
					ecf73dd7ab | 
							
								
								
									
										26
									
								
								.bithoundrc
									
									
									
									
									
								
							
							
						
						@ -1,26 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "mute": [
 | 
			
		||||
    ],
 | 
			
		||||
    "unused-ignores": [
 | 
			
		||||
      "almond",
 | 
			
		||||
      "d3-*",
 | 
			
		||||
      "leaflet",
 | 
			
		||||
      "moment",
 | 
			
		||||
      "navigo",
 | 
			
		||||
      "node-polyglot",
 | 
			
		||||
      "promise-polyfill",
 | 
			
		||||
      "rbush",
 | 
			
		||||
      "requirejs",
 | 
			
		||||
      "snabbdom"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "critics": {
 | 
			
		||||
    "wc": {
 | 
			
		||||
      "limit": 5000
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "ignore": [
 | 
			
		||||
    "polyfill.js"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,14 @@
 | 
			
		||||
kind: pipeline
 | 
			
		||||
name: meshviewer build
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
- name: docker  
 | 
			
		||||
  image: plugins/docker
 | 
			
		||||
  settings:
 | 
			
		||||
    repo: fftdf/meshviewer
 | 
			
		||||
    target: meshviewer
 | 
			
		||||
    username:
 | 
			
		||||
      from_secret: docker_username
 | 
			
		||||
    password:
 | 
			
		||||
      from_secret: docker_password
 | 
			
		||||
 | 
			
		||||
@ -2,15 +2,19 @@
 | 
			
		||||
 | 
			
		||||
# top-most EditorConfig file
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
charset = utf-8
 | 
			
		||||
 | 
			
		||||
# Get rid of whitespace to avoid diffs with a bunch of EOL changes
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
 | 
			
		||||
[*.{js,html,scss,json,yml,md}]
 | 
			
		||||
indent_size = 2
 | 
			
		||||
indent_style = space
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[assets/favicon/manifest.json]
 | 
			
		||||
indent_size = 4
 | 
			
		||||
 | 
			
		||||
@ -9,5 +9,6 @@ rules:
 | 
			
		||||
  "func-names": 0
 | 
			
		||||
  "guard-for-in": 0
 | 
			
		||||
  "no-undefined": 0
 | 
			
		||||
  "consistent-return": 0
 | 
			
		||||
  "no-nested-ternary": 0
 | 
			
		||||
  "no-extend-native": ["error", { "exceptions": ["String"] }]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -1,5 +1,6 @@
 | 
			
		||||
## Contributing is welcome
 | 
			
		||||
 | 
			
		||||
Pull requests are welcome without an opening an issue. If you unsure about the feature or implementation open an issue and
 | 
			
		||||
discuss your suggested changes. Meshviewer is a frontend application and the code needs to be loaded and 
 | 
			
		||||
perform on slow mobile devices with many nodes and clients.
 | 
			
		||||
Pull requests are welcome without the need of opening an issue. If you're unsure
 | 
			
		||||
about your feature or your implementation open an issue and discuss your
 | 
			
		||||
suggested changes. Meshviewer is a frontend application and the code needs to be
 | 
			
		||||
loaded fast and perform with many nodes and clients on slow mobile devices.
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,9 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<!--- Provide a general summary of the issue in the Title above -->
 | 
			
		||||
<!--- This template should help to improve the report, unneeded parts can be remvoed -->
 | 
			
		||||
 | 
			
		||||
@ -13,7 +19,7 @@
 | 
			
		||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
 | 
			
		||||
<!--- or ideas how to implement the addition or change -->
 | 
			
		||||
 | 
			
		||||
## Steps to Reproduce (for bugs)
 | 
			
		||||
## Steps to Reproduce
 | 
			
		||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
 | 
			
		||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
 | 
			
		||||
1.
 | 
			
		||||
@ -31,3 +37,6 @@
 | 
			
		||||
* Browser Name and version:
 | 
			
		||||
* Operating System and version (desktop or mobile):
 | 
			
		||||
* Link to your project:
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
<!--- If applicable, add screenshots to help explain your problem. -->
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,20 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<!--- Provide a general summary of the issue in the Title above -->
 | 
			
		||||
<!--- This template should help to improve the report, unneeded parts can be remvoed -->
 | 
			
		||||
 | 
			
		||||
## Is your feature request related to a problem? Please describe.
 | 
			
		||||
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
 | 
			
		||||
 | 
			
		||||
## Describe the solution you'd like
 | 
			
		||||
<!--- A clear and concise description of what you want to happen. -->
 | 
			
		||||
 | 
			
		||||
## Describe alternatives you've considered
 | 
			
		||||
<!--- A clear and concise description of any alternative solutions or features you've considered. -->
 | 
			
		||||
 | 
			
		||||
## Additional context
 | 
			
		||||
<!--- Add any other context or screenshots about the feature request here. -->
 | 
			
		||||
							
								
								
									
										17
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						@ -1,31 +1,24 @@
 | 
			
		||||
sudo: false
 | 
			
		||||
dist: trusty
 | 
			
		||||
 | 
			
		||||
language: node_js
 | 
			
		||||
 | 
			
		||||
node_js:
 | 
			
		||||
  - "7"
 | 
			
		||||
  - "12"
 | 
			
		||||
 | 
			
		||||
os:
 | 
			
		||||
  - linux
 | 
			
		||||
  - macosx
 | 
			
		||||
  - windows
 | 
			
		||||
  - osx
 | 
			
		||||
 | 
			
		||||
matrix:
 | 
			
		||||
  include:
 | 
			
		||||
  - node_js: 7
 | 
			
		||||
  - node_js: "8"
 | 
			
		||||
    os: linux
 | 
			
		||||
    env: USE_NPM=true
 | 
			
		||||
  - node_js: 6
 | 
			
		||||
  - node_js: "10"
 | 
			
		||||
    os: linux
 | 
			
		||||
  - node_js: 4
 | 
			
		||||
    os: linux
 | 
			
		||||
 | 
			
		||||
cache:
 | 
			
		||||
  yarn: true
 | 
			
		||||
 | 
			
		||||
before_install:
 | 
			
		||||
  - if [ "$USE_NPM" == "true" ]; then rm yarn.lock; fi
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - if git status | grep -q "modified.   \.travis\.yml"; then echo "Dirty yarn.lock"; exit 1; fi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										66
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@ -1,66 +0,0 @@
 | 
			
		||||
# Change Log
 | 
			
		||||
 | 
			
		||||
## Switched to rolling release
 | 
			
		||||
 | 
			
		||||
- All major changes can be found in README.md and everything else in git history https://github.com/ffrgb/meshviewer
 | 
			
		||||
- Lot of parts of codebase have been changed
 | 
			
		||||
 | 
			
		||||
## v4
 | 
			
		||||
 | 
			
		||||
- add a legend (map)
 | 
			
		||||
- new graph theme
 | 
			
		||||
- performance improvements in graph view
 | 
			
		||||
- various UI changes
 | 
			
		||||
- various map fixes
 | 
			
		||||
- moved config from config.js to config.json
 | 
			
		||||
- online/offline statistics
 | 
			
		||||
- define layers for map in config
 | 
			
		||||
- graph: zoom by keyboard (+ and - keys)
 | 
			
		||||
- direct links to graph and map views
 | 
			
		||||
 | 
			
		||||
### Bugfixes
 | 
			
		||||
 | 
			
		||||
- map works with little or no nodes
 | 
			
		||||
 | 
			
		||||
## v3
 | 
			
		||||
 | 
			
		||||
### Implemented enhancements:
 | 
			
		||||
 | 
			
		||||
- Make clients in map start at a random angle
 | 
			
		||||
- On statistics page: show how many nodes supply geoinformation
 | 
			
		||||
- Allow additional statistics (global and per node) configured in config.js
 | 
			
		||||
- Improve node count information (total, online, clients, ...)
 | 
			
		||||
- Show hardware model in link infobox
 | 
			
		||||
- Introduce maxAge setting
 | 
			
		||||
- Graph: show VPN links in grayscale
 | 
			
		||||
 | 
			
		||||
### Removed features:
 | 
			
		||||
 | 
			
		||||
- Don't show contact information in node lists
 | 
			
		||||
 | 
			
		||||
### Fixed bugs:
 | 
			
		||||
 | 
			
		||||
- Fixed off-by-one when drawing clients
 | 
			
		||||
- Match labels order to node order in map
 | 
			
		||||
- Statistics: count only nodes that are present
 | 
			
		||||
 | 
			
		||||
## v2
 | 
			
		||||
 | 
			
		||||
### General changes:
 | 
			
		||||
 | 
			
		||||
- License change from GPL 3 to AGPL 3
 | 
			
		||||
 | 
			
		||||
### Implemented enhancements:
 | 
			
		||||
 | 
			
		||||
- Improved performance on Firefox
 | 
			
		||||
- Labels in graph view
 | 
			
		||||
- infobox: link to geouri with node's coordinates
 | 
			
		||||
- infobox: show node id
 | 
			
		||||
- map: locate user
 | 
			
		||||
- map: adding custom layers from leaflet.providers
 | 
			
		||||
- nodelist: sort by uptime fixed
 | 
			
		||||
- graph: circles for clients
 | 
			
		||||
 | 
			
		||||
### Fixed bugs:
 | 
			
		||||
 | 
			
		||||
- Links disappeared on graph on refresh 
 | 
			
		||||
							
								
								
									
										21
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,21 @@
 | 
			
		||||
# builder
 | 
			
		||||
FROM node:12.13.1-stretch as builder
 | 
			
		||||
COPY . /mesh
 | 
			
		||||
WORKDIR /mesh
 | 
			
		||||
 | 
			
		||||
# show versions
 | 
			
		||||
RUN node --version && npm --version && yarn --version
 | 
			
		||||
 | 
			
		||||
# install gulp
 | 
			
		||||
RUN npm i gulp-cli -g
 | 
			
		||||
RUN npm i gulp -g
 | 
			
		||||
 | 
			
		||||
# run yarn for prerequisits
 | 
			
		||||
RUN yarn
 | 
			
		||||
 | 
			
		||||
# run gulp to build app
 | 
			
		||||
RUN gulp
 | 
			
		||||
 | 
			
		||||
# build docker container
 | 
			
		||||
FROM nginx:1.17.6-alpine as meshviewer
 | 
			
		||||
COPY --from=builder /mesh/build /usr/share/nginx/html/
 | 
			
		||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@ -1,64 +1,20 @@
 | 
			
		||||
# Meshviewer
 | 
			
		||||
[](https://travis-ci.org/ffrgb/meshviewer)
 | 
			
		||||
[](https://ci.freifunk-rhein-sieg.net/Freifunk-Troisdorf/meshviewer)
 | 
			
		||||
[](https://scrutinizer-ci.com/g/ffrgb/meshviewer/?branch=develop)
 | 
			
		||||
[](https://meshviewer.gitbooks.io/documentation/content/)
 | 
			
		||||
[](https://www.gnu.org/licenses/agpl-3.0)
 | 
			
		||||
[](https://www.gnu.org/licenses/agpl-3.0)
 | 
			
		||||
 | 
			
		||||
Meshviewer is an online visualization app to represent nodes and links on a map for Freifunk open mesh network.
 | 
			
		||||
 | 
			
		||||
#### Main differences to https://github.com/ffnord/meshviewer
 | 
			
		||||
_Some similar features might have been implemented/merged_
 | 
			
		||||
 | 
			
		||||
- Replaced router - including language, mode, node, link, location
 | 
			
		||||
- Leaflet upgraded to v1 - faster on mobile
 | 
			
		||||
- Forcegraph rewrite with d3.js v4
 | 
			
		||||
- Map layer modes (Allow to set a default layer based on time combined with a stylesheet)
 | 
			
		||||
- Automatic updates for selected node or list (incl. image stats cache-breaker)
 | 
			
		||||
- Node filter
 | 
			
		||||
- Zoom level for clicking on a node (`nodeZoom`) is definable independently from the maximum zoom level 22
 | 
			
		||||
- Formatted Code
 | 
			
		||||
- Translation support - https://crowdin.com/project/meshviewer - Contact us for new languages
 | 
			
		||||
  - Currently available: en, de, fr & ru
 | 
			
		||||
- Gulp inline for some css and js - fewer requests and instant load indicator
 | 
			
		||||
- Icon font with needed icons only
 | 
			
		||||
- Switch to Gulp (Tested with Node.js 4/6 LTS, 7 on Linux, 7 OSX & W**)
 | 
			
		||||
  - css and some js moved inline
 | 
			
		||||
- Yarn/npm in favour of bower
 | 
			
		||||
  - Load only moment.js without languages (Languages are included in translations)
 | 
			
		||||
  - unneeded components removed (es6-shim, tablesort, numeraljs, leaflet-providers, leaflet-label jshashes, chroma-js)
 | 
			
		||||
- RBush v2 - performance boost in last versions (positions, labels and clients on the map)
 | 
			
		||||
- Ruby dependency removed
 | 
			
		||||
- FixedCenter is required
 | 
			
		||||
- Sass-lint, scss and variables rewritten for easy customizations/adjustments
 | 
			
		||||
- Cross browser/device support improved (THX@BrowserStack)
 | 
			
		||||
- Yarn package manager in favour of npm (npm still works)
 | 
			
		||||
- Configurable reverse geocoding server
 | 
			
		||||
- [A lot more in the commit history](https://github.com/ffrgb/meshviewer/commits/develop)
 | 
			
		||||
 | 
			
		||||
### Demo
 | 
			
		||||
 | 
			
		||||
Embedded: https://regensburg.freifunk.net/netz/karte/  
 | 
			
		||||
Standalone: https://regensburg.freifunk.net/meshviewer/
 | 
			
		||||
 | 
			
		||||
## Documentation
 | 
			
		||||
 | 
			
		||||
Documentation moved to [meshviewer.gitbooks.io](https://meshviewer.gitbooks.io/documentation/content/).
 | 
			
		||||
 | 
			
		||||
- Read: https://meshviewer.gitbooks.io/documentation/content/
 | 
			
		||||
- PDF, Mobi, ePub & edit: https://www.gitbook.com/book/meshviewer/documentation/details
 | 
			
		||||
 | 
			
		||||
#### Why move the documentation?
 | 
			
		||||
 | 
			
		||||
- Search available
 | 
			
		||||
- Multiple pages
 | 
			
		||||
- Less doc commits, faster changes
 | 
			
		||||
- Export as PDF, Mobi, ePub
 | 
			
		||||
 | 
			
		||||
## Sponsoring / Supporting
 | 
			
		||||
 | 
			
		||||
- [BrowserStack](https://www.browserstack.com/) for providing an awesome testing service for hundreds of browsers
 | 
			
		||||
- [Travis CI](https://travis-ci.org/) for building meshviewer on every push and pull request
 | 
			
		||||
- [Travis CI](https://travis-ci.com/) for building meshviewer on every push and pull request
 | 
			
		||||
- [Scrutinizer CI](https://scrutinizer-ci.com/g/ffrgb/meshviewer/) for testing code quality on every push and pull request
 | 
			
		||||
- [Crowdin](https://crowdin.com/) for providing an easy non-developer translation environment
 | 
			
		||||
- [POEditor](https://poeditor.com/join/project/VZBjPNNic9) for providing an easy non-developer translation environment
 | 
			
		||||
 | 
			
		||||
These tools need a lot of infrastructures and provide a free account for open source software.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								app.js
									
									
									
									
									
								
							
							
						
						@ -9,21 +9,21 @@ require.config({
 | 
			
		||||
    'moment': '../node_modules/moment/moment',
 | 
			
		||||
    // d3 modules indirect dependencies
 | 
			
		||||
    // by d3-zoom: d3-drag
 | 
			
		||||
    'd3-ease': '../node_modules/d3-ease/build/d3-ease',
 | 
			
		||||
    'd3-transition': '../node_modules/d3-transition/build/d3-transition',
 | 
			
		||||
    'd3-color': '../node_modules/d3-color/build/d3-color',
 | 
			
		||||
    'd3-interpolate': '../node_modules/d3-interpolate/build/d3-interpolate',
 | 
			
		||||
    'd3-ease': '../node_modules/d3-ease/dist/d3-ease',
 | 
			
		||||
    'd3-transition': '../node_modules/d3-transition/dist/d3-transition',
 | 
			
		||||
    'd3-color': '../node_modules/d3-color/dist/d3-color',
 | 
			
		||||
    'd3-interpolate': '../node_modules/d3-interpolate/dist/d3-interpolate',
 | 
			
		||||
    // by d3-force
 | 
			
		||||
    'd3-collection': '../node_modules/d3-collection/build/d3-collection',
 | 
			
		||||
    'd3-dispatch': '../node_modules/d3-dispatch/build/d3-dispatch',
 | 
			
		||||
    'd3-quadtree': '../node_modules/d3-quadtree/build/d3-quadtree',
 | 
			
		||||
    'd3-timer': '../node_modules/d3-timer/build/d3-timer',
 | 
			
		||||
    'd3-collection': '../node_modules/d3-collection/dist/d3-collection',
 | 
			
		||||
    'd3-dispatch': '../node_modules/d3-dispatch/dist/d3-dispatch',
 | 
			
		||||
    'd3-quadtree': '../node_modules/d3-quadtree/dist/d3-quadtree',
 | 
			
		||||
    'd3-timer': '../node_modules/d3-timer/dist/d3-timer',
 | 
			
		||||
    // by d3-drag: d3-selection
 | 
			
		||||
    // d3 modules dependencies
 | 
			
		||||
    'd3-selection': '../node_modules/d3-selection/build/d3-selection',
 | 
			
		||||
    'd3-force': '../node_modules/d3-force/build/d3-force',
 | 
			
		||||
    'd3-zoom': '../node_modules/d3-zoom/build/d3-zoom',
 | 
			
		||||
    'd3-drag': '../node_modules/d3-drag/build/d3-drag',
 | 
			
		||||
    'd3-selection': '../node_modules/d3-selection/dist/d3-selection',
 | 
			
		||||
    'd3-force': '../node_modules/d3-force/dist/d3-force',
 | 
			
		||||
    'd3-zoom': '../node_modules/d3-zoom/dist/d3-zoom',
 | 
			
		||||
    'd3-drag': '../node_modules/d3-drag/dist/d3-drag',
 | 
			
		||||
    'snabbdom': '../node_modules/snabbdom/dist/snabbdom-patch',
 | 
			
		||||
    'rbush': '../node_modules/rbush/rbush',
 | 
			
		||||
    'helper': 'utils/helper'
 | 
			
		||||
@ -37,5 +37,5 @@ require.config({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
require(['main'], function (main) {
 | 
			
		||||
  main(jsonData);
 | 
			
		||||
  main();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.1 KiB  | 
| 
		 Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB  | 
| 
		 Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 485 B  | 
| 
		 Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 886 B  | 
| 
		 Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.2 KiB  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB  | 
@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "Meshviewer",
 | 
			
		||||
    "short_name": "Meshviewer",
 | 
			
		||||
    "icons": [
 | 
			
		||||
        {
 | 
			
		||||
            "src": "./android-chrome-192x192.png",
 | 
			
		||||
@ -14,5 +15,7 @@
 | 
			
		||||
    ],
 | 
			
		||||
    "theme_color": "#dc0067",
 | 
			
		||||
    "background_color": "#dc0067",
 | 
			
		||||
    "display": "standalone"
 | 
			
		||||
    "start_url": ".",
 | 
			
		||||
    "display": "standalone",
 | 
			
		||||
    "orientation": "portrait"
 | 
			
		||||
}
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
{"result":{"status":"success"},"favicon":{"package_url":"https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/favicons.zip","files_urls":["https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/android-chrome-192x192.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/android-chrome-512x512.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/apple-touch-icon.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/browserconfig.xml","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon-16x16.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon-32x32.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon.ico","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/manifest.json","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-144x144.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-150x150.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-310x150.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-310x310.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-70x70.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/safari-pinned-tab.svg"],"html_code":"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"./apple-touch-icon.png\">\n<link rel=\"icon\" type=\"image/png\" href=\"./favicon-32x32.png\" sizes=\"32x32\">\n<link rel=\"icon\" type=\"image/png\" href=\"./favicon-16x16.png\" sizes=\"16x16\">\n<link rel=\"manifest\" href=\"./manifest.json\">\n<link rel=\"mask-icon\" href=\"./safari-pinned-tab.svg\" color=\"#dc0067\">\n<link rel=\"shortcut icon\" href=\"./favicon.ico\">\n<meta name=\"apple-mobile-web-app-title\" content=\"Meshviewer\">\n<meta name=\"application-name\" content=\"Meshviewer\">\n<meta name=\"msapplication-TileColor\" content=\"#dc0067\">\n<meta name=\"msapplication-TileImage\" content=\"./mstile-144x144.png\">\n<meta name=\"msapplication-config\" content=\"./browserconfig.xml\">\n<meta name=\"theme-color\" content=\"#dc0067\">","compression":"true","overlapping_markups":["link[rel=\"apple-touch-icon\"]","meta[name=\"apple-mobile-web-app-title\"]","link[rel=\"shortcut\"]","link[rel=\"shortcut icon\"]","link[rel=\"icon\",sizes=\"16x16\"]","link[rel=\"icon\",sizes=\"32x32\"]","meta[name=\"msapplication-TileColor\"]","meta[name=\"msapplication-TileImage\"]","meta[name=\"msapplication-config\"]","meta[name=\"application-name\"]","link[rel=\"manifest\"]","meta[name=\"theme-color\"]","link[rel=\"mask-icon\"]"]},"files_location":{"type":"path","path":"."},"preview_picture_url":"https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/favicon_preview.png","version":"0.14"}
 | 
			
		||||
{"result":{"status":"success"},"favicon":{"package_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_package_v0.16.zip","files_urls":["https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-192x192.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-512x512.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/apple-touch-icon.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/browserconfig.xml","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-16x16.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-32x32.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon.ico","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-144x144.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-150x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x310.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-70x70.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/safari-pinned-tab.svg","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/site.webmanifest"],"html_code":"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"./apple-touch-icon.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"./favicon-32x32.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"./favicon-16x16.png\">\n<link rel=\"manifest\" href=\"./site.webmanifest\">\n<link rel=\"mask-icon\" href=\"./safari-pinned-tab.svg\" color=\"#dc0067\">\n<link rel=\"shortcut icon\" href=\"./favicon.ico\">\n<meta name=\"apple-mobile-web-app-title\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"application-name\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"msapplication-TileColor\" content=\"#dc0067\">\n<meta name=\"msapplication-TileImage\" content=\"./mstile-144x144.png\">\n<meta name=\"msapplication-config\" content=\"./browserconfig.xml\">\n<meta name=\"theme-color\" content=\"#dc0067\">","compression":"true","overlapping_markups":["link[rel=\"apple-touch-icon\"]","meta[name=\"apple-mobile-web-app-title\"]","link[rel=\"shortcut\"]","link[rel=\"shortcut icon\"]","link[rel=\"icon\",sizes=\"16x16\"]","link[rel=\"icon\",sizes=\"32x32\"]","meta[name=\"msapplication-TileColor\"]","meta[name=\"msapplication-TileImage\"]","meta[name=\"msapplication-config\"]","meta[name=\"application-name\"]","link[rel=\"manifest\"]","meta[name=\"theme-color\"]","link[rel=\"mask-icon\"]"]},"files_location":{"type":"path","path":"."},"preview_picture_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_preview.png","version":"0.16"}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/icons/fonts/meshviewer.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								assets/icons/fonts/meshviewer.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								assets/icons/fonts/meshviewer.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -7,9 +7,9 @@ $cache-breaker: unique-id();
 | 
			
		||||
  font-family: 'ionicons';
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
  src: url('fonts/icon.woff2?rel=#{$cache-breaker}') format('woff2'),
 | 
			
		||||
  url('fonts/icon.woff?rel=#{$cache-breaker}') format('woff'),
 | 
			
		||||
  url('fonts/icon.ttf?rel=#{$cache-breaker}') format('truetype');
 | 
			
		||||
  src: url('fonts/meshviewer.woff2?rel=#{$cache-breaker}') format('woff2'),
 | 
			
		||||
  url('fonts/meshviewer.woff?rel=#{$cache-breaker}') format('woff'),
 | 
			
		||||
  url('fonts/meshviewer.ttf?rel=#{$cache-breaker}') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[class^='ion-'],
 | 
			
		||||
@ -49,3 +49,5 @@ $cache-breaker: unique-id();
 | 
			
		||||
@include icon('arrow-resize', '\f264');
 | 
			
		||||
@include icon('arrow-left-c', '\f108');
 | 
			
		||||
@include icon('arrow-right-c', '\f10b');
 | 
			
		||||
@include icon('full-enter', '\e901');
 | 
			
		||||
@include icon('full-exit', '\e900');
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										194
									
								
								config.default.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,194 @@
 | 
			
		||||
module.exports = function () {
 | 
			
		||||
  return {
 | 
			
		||||
    'reverseGeocodingApi': 'https://nominatim.openstreetmap.org/reverse',
 | 
			
		||||
    'maxAge': 14,
 | 
			
		||||
    'maxAgeAlert': 3,
 | 
			
		||||
    'nodeZoom': 20,
 | 
			
		||||
    'labelZoom': 13,
 | 
			
		||||
    'clientZoom': 15,
 | 
			
		||||
    'fullscreen': true,
 | 
			
		||||
    'fullscreenFrame': true,
 | 
			
		||||
    'nodeAttr': [
 | 
			
		||||
      // value can be a node attribute (1 depth) or a a function in utils/node with prefix show
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.status',
 | 
			
		||||
        'value': 'Status'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.gateway',
 | 
			
		||||
        'value': 'Gateway'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.coordinates',
 | 
			
		||||
        'value': 'GeoURI'
 | 
			
		||||
      },
 | 
			
		||||
      //    {
 | 
			
		||||
      //      "name": "node.contact",
 | 
			
		||||
      //      "value": "owner"
 | 
			
		||||
      //    },
 | 
			
		||||
 | 
			
		||||
      // Examples for functions
 | 
			
		||||
      // {
 | 
			
		||||
      //   // no name will remove first column
 | 
			
		||||
      //   'value': function (d) {
 | 
			
		||||
      //     var moment = require('moment');
 | 
			
		||||
      //     var V = require('snabbdom').default;
 | 
			
		||||
      //     return V.h('td', { props: { colSpan: 2 }, style: { background: '#49a' } },
 | 
			
		||||
      //       _.t('sidebar.nodeOnline') + ' translate, ' + moment(d.firstseen).get('month') +
 | 
			
		||||
      //       ' Month require libs like moment, access config ' + config.siteName);
 | 
			
		||||
      //   }
 | 
			
		||||
      // },
 | 
			
		||||
      // {
 | 
			
		||||
      //   'name': 'Neighbour first seen',
 | 
			
		||||
      //   'value': function (d, nodeDict) {
 | 
			
		||||
      //     return nodeDict[d.gateway_nexthop].firstseen.format() + 'access node object';
 | 
			
		||||
      //   }
 | 
			
		||||
      // },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.hardware',
 | 
			
		||||
        'value': 'model'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.primaryMac',
 | 
			
		||||
        'value': 'mac'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.firmware',
 | 
			
		||||
        'value': 'Firmware'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.uptime',
 | 
			
		||||
        'value': 'Uptime'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.firstSeen',
 | 
			
		||||
        'value': 'FirstSeen'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.systemLoad',
 | 
			
		||||
        'value': 'Load'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.ram',
 | 
			
		||||
        'value': 'RAM'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.ipAddresses',
 | 
			
		||||
        'value': 'IPs'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.update',
 | 
			
		||||
        'value': 'Autoupdate'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.domain',
 | 
			
		||||
        'value': 'Domain'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'node.clients',
 | 
			
		||||
        'value': 'Clients'
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    'supportedLocale': [
 | 
			
		||||
      'en',
 | 
			
		||||
      'de',
 | 
			
		||||
      'cz',
 | 
			
		||||
      'fr',
 | 
			
		||||
      'tr',
 | 
			
		||||
      'ru'
 | 
			
		||||
    ],
 | 
			
		||||
    // Color configs
 | 
			
		||||
    'icon': {
 | 
			
		||||
      'base': {
 | 
			
		||||
        'fillOpacity': 0.6,
 | 
			
		||||
        'opacity': 0.6,
 | 
			
		||||
        'weight': 2,
 | 
			
		||||
        'radius': 6,
 | 
			
		||||
        'className': 'stroke-first'
 | 
			
		||||
      },
 | 
			
		||||
      'online': {
 | 
			
		||||
        'color': '#1566A9',
 | 
			
		||||
        'fillColor': '#1566A9'
 | 
			
		||||
      },
 | 
			
		||||
      'offline': {
 | 
			
		||||
        'color': '#D43E2A',
 | 
			
		||||
        'fillColor': '#D43E2A',
 | 
			
		||||
        'radius': 3
 | 
			
		||||
      },
 | 
			
		||||
      'lost': {
 | 
			
		||||
        'color': '#D43E2A',
 | 
			
		||||
        'fillColor': '#D43E2A',
 | 
			
		||||
        'radius': 4
 | 
			
		||||
      },
 | 
			
		||||
      'alert': {
 | 
			
		||||
        'color': '#D43E2A',
 | 
			
		||||
        'fillColor': '#D43E2A',
 | 
			
		||||
        'radius': 5
 | 
			
		||||
      },
 | 
			
		||||
      'new': {
 | 
			
		||||
        'color': '#1566A9',
 | 
			
		||||
        'fillColor': '#93E929'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    'client': {
 | 
			
		||||
      'wifi24': 'rgba(220, 0, 103, 0.7)',
 | 
			
		||||
      'wifi5': 'rgba(10, 156, 146, 0.7)',
 | 
			
		||||
      'other': 'rgba(227, 166, 25, 0.7)'
 | 
			
		||||
    },
 | 
			
		||||
    'map': {
 | 
			
		||||
      'labelNewColor': '#459c18',
 | 
			
		||||
      'tqFrom': '#F02311',
 | 
			
		||||
      'tqTo': '#04C714',
 | 
			
		||||
      'highlightNode': {
 | 
			
		||||
        'color': '#ad2358',
 | 
			
		||||
        'weight': 8,
 | 
			
		||||
        'fillOpacity': 1,
 | 
			
		||||
        'opacity': 0.4,
 | 
			
		||||
        'className': 'stroke-first'
 | 
			
		||||
      },
 | 
			
		||||
      'highlightLink': {
 | 
			
		||||
        'weight': 4,
 | 
			
		||||
        'opacity': 1,
 | 
			
		||||
        'dashArray': '5, 10'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    'forceGraph': {
 | 
			
		||||
      'nodeColor': '#fff',
 | 
			
		||||
      'nodeOfflineColor': '#D43E2A',
 | 
			
		||||
      'highlightColor': 'rgba(255, 255, 255, 0.2)',
 | 
			
		||||
      'labelColor': '#fff',
 | 
			
		||||
      'tqFrom': '#770038',
 | 
			
		||||
      'tqTo': '#dc0067',
 | 
			
		||||
      'zoomModifier': 1
 | 
			
		||||
    },
 | 
			
		||||
    'locate': {
 | 
			
		||||
      'outerCircle': {
 | 
			
		||||
        'stroke': false,
 | 
			
		||||
        'color': '#4285F4',
 | 
			
		||||
        'opacity': 1,
 | 
			
		||||
        'fillOpacity': 0.3,
 | 
			
		||||
        'clickable': false,
 | 
			
		||||
        'radius': 16
 | 
			
		||||
      },
 | 
			
		||||
      'innerCircle': {
 | 
			
		||||
        'stroke:': true,
 | 
			
		||||
        'color': '#ffffff',
 | 
			
		||||
        'fillColor': '#4285F4',
 | 
			
		||||
        'weight': 1.5,
 | 
			
		||||
        'clickable': false,
 | 
			
		||||
        'opacity': 1,
 | 
			
		||||
        'fillOpacity': 1,
 | 
			
		||||
        'radius': 7
 | 
			
		||||
      },
 | 
			
		||||
      'accuracyCircle': {
 | 
			
		||||
        'stroke': true,
 | 
			
		||||
        'color': '#4285F4',
 | 
			
		||||
        'weight': 1,
 | 
			
		||||
        'clickable': false,
 | 
			
		||||
        'opacity': 0.7,
 | 
			
		||||
        'fillOpacity': 0.2
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    'cacheBreaker': '<!-- inject:cache-breaker -->'
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
// Gulp will remove all comments
 | 
			
		||||
{
 | 
			
		||||
  "reverseGeocodingApi": "https://nominatim.openstreetmap.org/reverse",
 | 
			
		||||
  "maxAge": 14,
 | 
			
		||||
  "maxAgeAlert": 3,
 | 
			
		||||
  "nodeZoom": 18,
 | 
			
		||||
  "labelZoom": 13,
 | 
			
		||||
  "clientZoom": 15,
 | 
			
		||||
  "nodeInfobox": {
 | 
			
		||||
    "contact": false,
 | 
			
		||||
    "hardwareUsage": true
 | 
			
		||||
  },
 | 
			
		||||
  "supportedLocale": [
 | 
			
		||||
    "en",
 | 
			
		||||
    "de",
 | 
			
		||||
    "fr",
 | 
			
		||||
    "ru"
 | 
			
		||||
  ],
 | 
			
		||||
  "icon": {
 | 
			
		||||
    "base": {
 | 
			
		||||
      "fillOpacity": 0.6,
 | 
			
		||||
      "opacity": 0.6,
 | 
			
		||||
      "weight": 2,
 | 
			
		||||
      "radius": 6,
 | 
			
		||||
      "className": "stroke-first"
 | 
			
		||||
    },
 | 
			
		||||
    "online": {
 | 
			
		||||
      "color": "#1566A9",
 | 
			
		||||
      "fillColor": "#1566A9"
 | 
			
		||||
    },
 | 
			
		||||
    "offline": {
 | 
			
		||||
      "color": "#D43E2A",
 | 
			
		||||
      "fillColor": "#D43E2A",
 | 
			
		||||
      "radius": 3
 | 
			
		||||
    },
 | 
			
		||||
    "lost": {
 | 
			
		||||
      "color": "#D43E2A",
 | 
			
		||||
      "fillColor": "#D43E2A",
 | 
			
		||||
      "radius": 4
 | 
			
		||||
    },
 | 
			
		||||
    "alert": {
 | 
			
		||||
      "color": "#D43E2A",
 | 
			
		||||
      "fillColor": "#D43E2A",
 | 
			
		||||
      "radius": 5
 | 
			
		||||
    },
 | 
			
		||||
    "new": {
 | 
			
		||||
      "color": "#1566A9",
 | 
			
		||||
      "fillColor": "#93E929"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "cacheBreaker": "<!-- inject:cache-breaker -->"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,89 @@
 | 
			
		||||
module.exports = function () {
 | 
			
		||||
  return {
 | 
			
		||||
    // Variables are NODE_ID and NODE_NAME (only a-z0-9\- other chars are replaced with _)
 | 
			
		||||
    'nodeInfos': [
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'Clientstatistik',
 | 
			
		||||
        'href': 'https://statistik.freifunk-troisdorf.de/d/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60',
 | 
			
		||||
        'image': 'https://statistik.freifunk-troisdorf.de/render/d-solo/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60&theme=light&panelId=1&width=1000&height=500&tz=Europe%2FBerlin',
 | 
			
		||||
        'title': 'Knoten {NODE_ID}'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'Traffic',
 | 
			
		||||
        'href': 'https://statistik.freifunk-troisdorf.de/d/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60',
 | 
			
		||||
        'image': 'https://statistik.freifunk-troisdorf.de/render/d-solo/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60&theme=light&panelId=2&width=1000&height=500&tz=Europe%2FBerlin',
 | 
			
		||||
        'title': 'Knoten {NODE_ID}'
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    // Array of data provider are supported
 | 
			
		||||
    'dataPath': [
 | 
			
		||||
    //  'https://map.freifunk-troisdorf.de/data/tdf4/',
 | 
			
		||||
    //  'https://map.freifunk-troisdorf.de/data/tdf5/',
 | 
			
		||||
    //  'https://map.freifunk-troisdorf.de/data/tdf6/',
 | 
			
		||||
    //  'https://map.freifunk-troisdorf.de/data/rifu/'
 | 
			
		||||
        'https://map.freifunk-troisdorf.de/data/api/'
 | 
			
		||||
    ],
 | 
			
		||||
    'siteName': 'Freifunk Troisdorf',
 | 
			
		||||
    'maxAge': 7,
 | 
			
		||||
    'mapLayers': [
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'Freifunk Rhein-Sieg',
 | 
			
		||||
        // Please ask Freifunk Rhein Sieg before using its tile server c- example with retina tiles
 | 
			
		||||
        'url': 'https://tile.freifunk-rhein-sieg.net/tile/{z}/{x}/{y}.png',
 | 
			
		||||
        'config': {
 | 
			
		||||
          'maxZoom': 20,
 | 
			
		||||
          'attribution': '<a href="https://freifunk-rhein-sieg.net/" target="_blank">© Freifunk-Rhein-Sieg</a> <a href="http://www.openstreetmap.org/about/" target="_blank">© OpenStreetMap contributors</a>'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'name': 'OSM',
 | 
			
		||||
        'url': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
 | 
			
		||||
        'config': {
 | 
			
		||||
          'maxZoom': 20,
 | 
			
		||||
          'attribution': '<a href="http://www.openmaptiles.org/" target="_blank">© OpenMapTiles</a> <a href="http://www.openstreetmap.org/about/" target="_blank">© OpenStreetMap contributors</a>'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    // Set a visible frame
 | 
			
		||||
    'fixedCenter': [
 | 
			
		||||
    // Northwest
 | 
			
		||||
      [
 | 
			
		||||
        50.8428,
 | 
			
		||||
        7.0367
 | 
			
		||||
      ],
 | 
			
		||||
      // Southeast
 | 
			
		||||
      [
 | 
			
		||||
        50.776,
 | 
			
		||||
        7.1919
 | 
			
		||||
      ]
 | 
			
		||||
    ],
 | 
			
		||||
    'domainNames': [
 | 
			
		||||
      {
 | 
			
		||||
        'site': 'tdf',
 | 
			
		||||
        'name': 'Troisdorf'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'site': 'inn',
 | 
			
		||||
        'name': 'Innenstadt'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'site': 'rifu',
 | 
			
		||||
        'name': 'Richtfunk'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'site': 'flu',
 | 
			
		||||
        'name': 'Soziale Netze'
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    'linkList': [
 | 
			
		||||
      {
 | 
			
		||||
        'title': 'Impressum',
 | 
			
		||||
        'href': 'http://freifunk-troisdorf.de/kontakt/impressum/'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'title': 'Datenschutz',
 | 
			
		||||
        'href': 'http://freifunk-troisdorf.de/datenschutz/'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										115
									
								
								config.json
									
									
									
									
									
								
							
							
						
						@ -1,115 +0,0 @@
 | 
			
		||||
// Gulp will remove all comments
 | 
			
		||||
{
 | 
			
		||||
  // Variables are NODE_ID and NODE_NAME (only a-z0-9\- other chars are replaced with _)
 | 
			
		||||
  "nodeInfos": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Clientstatistik",
 | 
			
		||||
      "href": "https://regensburg.freifunk.net/netz/statistik/node/{NODE_ID}/",
 | 
			
		||||
      "image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-all-nodes?panelId=1&from=now-7d&var-nodeid={NODE_ID}&var-host={NODE_NAME}&width=650&height=350&theme=light&_t={TIME}",
 | 
			
		||||
      "title": "Knoten {NODE_ID} - weiteren Statistiken"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Trafficstatistik",
 | 
			
		||||
      "href": "https://regensburg.freifunk.net/netz/statistik/node/{NODE_ID}/",
 | 
			
		||||
      "image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-all-nodes?panelId=2&from=now-7d&var-nodeid={NODE_ID}&var-host={NODE_NAME}&width=650&height=350&theme=light&_t={TIME}",
 | 
			
		||||
      "title": "Knoten {NODE_ID} - weiteren Statistiken"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "globalInfos": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Statistik",
 | 
			
		||||
      "href": "https://regensburg.freifunk.net/netz/statistik/",
 | 
			
		||||
      "image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-network-wide-stats?panelId=11&from=now-1y&width=600&height=350&theme=light",
 | 
			
		||||
      "title": "Jahresstatistik - weiteren Statistiken"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  // String or array of data provider are supported
 | 
			
		||||
  "dataPath": "https://regensburg.freifunk.net/data/",
 | 
			
		||||
  "siteName": "Freifunk Regensburg",
 | 
			
		||||
  "mapLayers": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Freifunk Regensburg",
 | 
			
		||||
      // Please ask Freifunk Regensburg before using its tile server c- example with retina tiles
 | 
			
		||||
      "url": "https://{s}.tiles.ffrgb.net/{z}/{x}/{y}{retina}.png",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "maxZoom": 22,
 | 
			
		||||
        "subdomains": "1234",
 | 
			
		||||
        "attribution": "<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\" target=\"_blank\">© OpenStreetMap</a> <a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">Improve this map</a>",
 | 
			
		||||
        "start": 6
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Freifunk Regensburg Night",
 | 
			
		||||
      // Please ask Freifunk Regensburg before using its tile server - example with retina and dark tiles
 | 
			
		||||
      "url": "https://{s}.tiles.ffrgb.net/n/{z}/{x}/{y}{retina}.png",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "maxZoom": 22,
 | 
			
		||||
        "subdomains": "1234",
 | 
			
		||||
        "attribution": "<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\" target=\"_blank\">© OpenStreetMap</a> <a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">Improve this map</a>",
 | 
			
		||||
        "mode": "night",
 | 
			
		||||
        "start": 19,
 | 
			
		||||
        "end": 7
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "OpenStreetMap.HOT",
 | 
			
		||||
      "url": "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "maxZoom": 19,
 | 
			
		||||
        "attribution": "© Openstreetmap France | © <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "HERE",
 | 
			
		||||
      // Please use your own API key - Free plan is on right side after the pay plans
 | 
			
		||||
      "url": "https://{s}.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?app_id=YOUR_KEY&app_code=YOUR_CODE&lg=deu",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "attribution": "Map © 1987-2014 <a href=\"http://developer.here.com\">HERE</a>",
 | 
			
		||||
        "subdomains": "1234",
 | 
			
		||||
        "maxZoom": 20
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Esri.WorldImagery",
 | 
			
		||||
      "url": "//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "maxZoom": 20,
 | 
			
		||||
        "attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "HERE.hybridDay",
 | 
			
		||||
      // Please use your own API key - Free plan is on right side after the pay plans
 | 
			
		||||
      "url": "https://{s}.aerial.maps.api.here.com/maptile/2.1/maptile/newest/{variant}/{z}/{x}/{y}/256/png8?app_id=YOUR_KEY&app_code=YOUR_CODE&lg=deu",
 | 
			
		||||
      "config": {
 | 
			
		||||
        "attribution": "Map © 1987-2014 <a href=\"http://developer.here.com\">HERE</a>",
 | 
			
		||||
        "subdomains": "1234",
 | 
			
		||||
        "variant": "hybrid.day",
 | 
			
		||||
        "maxZoom": 20
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  // Set a visible frame
 | 
			
		||||
  "fixedCenter": [
 | 
			
		||||
    // Northwest
 | 
			
		||||
    [
 | 
			
		||||
      49.3522,
 | 
			
		||||
      11.7752
 | 
			
		||||
    ],
 | 
			
		||||
    // Southeast
 | 
			
		||||
    [
 | 
			
		||||
      48.7480,
 | 
			
		||||
      12.8917
 | 
			
		||||
    ]
 | 
			
		||||
  ],
 | 
			
		||||
  "siteNames": [
 | 
			
		||||
    {
 | 
			
		||||
      "site": "ffrgb-bat15",
 | 
			
		||||
      "name": "Regensburg"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "site": "ffrgb",
 | 
			
		||||
      "name": "Regensburg"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
files:
 | 
			
		||||
  - source: /locale/en.json
 | 
			
		||||
    translation: /locale/%two_letters_code%.json
 | 
			
		||||
@ -8,11 +8,11 @@ module.exports = function () {
 | 
			
		||||
      sass: 'scss/**/*.scss',
 | 
			
		||||
      javascript: ['./app.js', 'lib/**/*.js'],
 | 
			
		||||
      json: 'locale/*.json',
 | 
			
		||||
      html: ['html/*.html', './config*.json']
 | 
			
		||||
      html: ['html/*.html', './config*.js']
 | 
			
		||||
    },
 | 
			
		||||
    clean: [build + '/*.map', build + '/vendor', build + '/main.css'],
 | 
			
		||||
    autoprefixer: ['> 1% in DE'],
 | 
			
		||||
    browsersync: {
 | 
			
		||||
      open: false,
 | 
			
		||||
      server: {
 | 
			
		||||
        baseDir: build
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,12 @@ module.exports = function (gulp, plugins, config) {
 | 
			
		||||
  return function copy() {
 | 
			
		||||
    gulp.src(['html/*.html', 'assets/favicon/*'])
 | 
			
		||||
      .pipe(gulp.dest(config.build));
 | 
			
		||||
    gulp.src(['assets/logo.svg'])
 | 
			
		||||
    gulp.src(['assets/logo.svg', 'service-worker.js'])
 | 
			
		||||
      .pipe(gulp.dest(config.build));
 | 
			
		||||
    gulp.src(['node_modules/promise-polyfill/promise.js', 'polyfill.js'])
 | 
			
		||||
    gulp.src(['polyfill.js'])
 | 
			
		||||
      .pipe(gulp.dest(config.build + '/vendor'));
 | 
			
		||||
    gulp.src(['node_modules/promise-polyfill/dist/polyfill.js'])
 | 
			
		||||
      .pipe(gulp.dest(config.build + '/vendor/promise'));
 | 
			
		||||
    return gulp.src(['assets/fonts/*', 'assets/icons/fonts/*'])
 | 
			
		||||
      .pipe(gulp.dest(config.build + '/fonts'));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ module.exports = function (gulp, plugins, config) {
 | 
			
		||||
      design: {
 | 
			
		||||
        ios: {
 | 
			
		||||
          pictureAspect: 'backgroundAndMargin',
 | 
			
		||||
          backgroundColor: '#000000',
 | 
			
		||||
          backgroundColor: '#ffffff',
 | 
			
		||||
          margin: '14%',
 | 
			
		||||
          assets: {
 | 
			
		||||
            ios6AndPriorIcons: false,
 | 
			
		||||
@ -19,7 +19,7 @@ module.exports = function (gulp, plugins, config) {
 | 
			
		||||
        },
 | 
			
		||||
        desktopBrowser: {},
 | 
			
		||||
        windows: {
 | 
			
		||||
          pictureAspect: 'noChange',
 | 
			
		||||
          pictureAspect: 'whiteSilhouette',
 | 
			
		||||
          backgroundColor: '#dc0067',
 | 
			
		||||
          onConflict: 'override',
 | 
			
		||||
          assets: {
 | 
			
		||||
@ -39,7 +39,7 @@ module.exports = function (gulp, plugins, config) {
 | 
			
		||||
          manifest: {
 | 
			
		||||
            name: 'Meshviewer',
 | 
			
		||||
            display: 'standalone',
 | 
			
		||||
            orientation: 'notSet',
 | 
			
		||||
            orientation: 'portrait',
 | 
			
		||||
            onConflict: 'override',
 | 
			
		||||
            declared: true
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,52 @@
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
 | 
			
		||||
// stringify functions https://gist.github.com/cowboy/3749767
 | 
			
		||||
var stringify = function (obj) {
 | 
			
		||||
  var placeholder = '____PLACEHOLDER____';
 | 
			
		||||
  var fns = [];
 | 
			
		||||
  var json = JSON.stringify(obj, function (key, value) {
 | 
			
		||||
    if (typeof value === 'function') {
 | 
			
		||||
      fns.push(value);
 | 
			
		||||
      return placeholder;
 | 
			
		||||
    }
 | 
			
		||||
    return value;
 | 
			
		||||
  }, 2);
 | 
			
		||||
  json = json.replace(new RegExp('"' + placeholder + '"', 'g'), function () {
 | 
			
		||||
    return fns.shift();
 | 
			
		||||
  });
 | 
			
		||||
  return json;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = function (gulp, plugins, config, env) {
 | 
			
		||||
  return function html() {
 | 
			
		||||
    return gulp.src(env.production() ? config.build + '/*.html' : 'html/*.html')
 | 
			
		||||
      .pipe(plugins.inject(gulp.src(['config.json']), {
 | 
			
		||||
      .pipe(plugins.realFavicon.injectFaviconMarkups(JSON.parse(fs.readFileSync(config.faviconData)).favicon.html_code))
 | 
			
		||||
      .pipe(env.production(plugins.inlineSource({ compress: false })))
 | 
			
		||||
      .pipe(plugins.inject(gulp.src(['config.js']), {
 | 
			
		||||
        removeTags: true,
 | 
			
		||||
        starttag: '<!-- inject:config -->',
 | 
			
		||||
        transform: function (filePath, customConfig) {
 | 
			
		||||
          var defaultConfig = fs.readFileSync('config.default.json', 'utf8');
 | 
			
		||||
          var buildConfig = Object.assign(
 | 
			
		||||
            JSON.parse(JSON.minify(defaultConfig)),
 | 
			
		||||
            JSON.parse(JSON.minify(customConfig.contents.toString('utf8')))
 | 
			
		||||
          );
 | 
			
		||||
          return '<script>var jsonData =' +
 | 
			
		||||
            JSON.stringify(buildConfig)
 | 
			
		||||
        transform: function () {
 | 
			
		||||
          delete require.cache[require.resolve('../../config.default')];
 | 
			
		||||
          delete require.cache[require.resolve('../../config')];
 | 
			
		||||
          var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
 | 
			
		||||
          return '<title>' + buildConfig.siteName + ' - loading...</title>' +
 | 
			
		||||
            '<script>window.config =' +
 | 
			
		||||
            stringify(buildConfig)
 | 
			
		||||
              .replace('<!-- inject:cache-breaker -->',
 | 
			
		||||
                Math.random().toString(12).substring(7)) +
 | 
			
		||||
            ';</script>'
 | 
			
		||||
            ;
 | 
			
		||||
            ';</script>';
 | 
			
		||||
        }
 | 
			
		||||
      }))
 | 
			
		||||
      .pipe(plugins.inject(gulp.src(['config.js']), {
 | 
			
		||||
        removeTags: true,
 | 
			
		||||
        starttag: '<!-- inject:title -->',
 | 
			
		||||
        transform: function () {
 | 
			
		||||
          delete require.cache[require.resolve('../../config.default')];
 | 
			
		||||
          delete require.cache[require.resolve('../../config')];
 | 
			
		||||
          var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
 | 
			
		||||
          return buildConfig.siteName;
 | 
			
		||||
        }
 | 
			
		||||
      }))
 | 
			
		||||
      .pipe(env.production(plugins.kyhInlineSource({ compress: false })))
 | 
			
		||||
      .pipe(plugins.realFavicon.injectFaviconMarkups(JSON.parse(fs.readFileSync(config.faviconData)).favicon.html_code))
 | 
			
		||||
      .pipe(plugins.cacheBust({
 | 
			
		||||
        type: 'timestamp'
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,10 @@ module.exports = function (gulp, plugins, config, env) {
 | 
			
		||||
    return gulp.src('app.js')
 | 
			
		||||
      .pipe(env.development(plugins.sourcemaps.init()))
 | 
			
		||||
      .pipe(plugins.requirejsOptimize(env.production() ? config.requireJs.prod : config.requireJs.dev))
 | 
			
		||||
      .pipe(env.production(plugins.uglify({ preserveComments: 'license' })))
 | 
			
		||||
      .on('error', function () {
 | 
			
		||||
        this.emit('end');
 | 
			
		||||
      })
 | 
			
		||||
      .pipe(env.production(plugins.uglify({ output: { comments: 'all' } })))
 | 
			
		||||
      .pipe(env.development(plugins.sourcemaps.write('.', { addComment: true })))
 | 
			
		||||
      .pipe(gulp.dest(config.build));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,9 @@ module.exports = function (gulp, plugins, config, env) {
 | 
			
		||||
        outputStyle: 'compressed',
 | 
			
		||||
        sourceMap: false
 | 
			
		||||
      }))
 | 
			
		||||
      .on('error', function () {
 | 
			
		||||
        this.emit('end');
 | 
			
		||||
      })
 | 
			
		||||
      .pipe(plugins.autoprefixer({
 | 
			
		||||
        browsers: config.autoprefixer
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,43 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<html itemscope itemtype="http://schema.org/WebPage">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
 | 
			
		||||
  <!--<meta name="image" content="https://regensburg.freifunk.net/meshviewer/apple-touch-icon.png">-->
 | 
			
		||||
 | 
			
		||||
  <meta itemprop="name" content="<!-- inject:title --><!-- endinject --> Meshviewer">
 | 
			
		||||
  <meta name="description" itemprop="description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
 | 
			
		||||
  <!--Uncomment & adjust local urls-->
 | 
			
		||||
  <!--<meta itemprop="image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
 | 
			
		||||
 | 
			
		||||
  <!--<meta property="business:contact_data:locality" content="Regensburg">-->
 | 
			
		||||
  <!--<meta property="business:contact_data:region" content="Bayern">-->
 | 
			
		||||
  <meta property="business:contact_data:country_name" content="Germany">
 | 
			
		||||
 | 
			
		||||
  <meta name="twitter:card" content="summary">
 | 
			
		||||
  <meta name="twitter:site" content="@freifunk">
 | 
			
		||||
 | 
			
		||||
  <meta name="og:title" content="<!-- inject:title --><!-- endinject -->">
 | 
			
		||||
  <meta name="og:description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
 | 
			
		||||
  <!--<meta name="og:image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
 | 
			
		||||
  <!--<meta name="og:url" content="https://regensburg.freifunk.net/meshviewer/">-->
 | 
			
		||||
  <meta name="og:site_name" content="<!-- inject:title --><!-- endinject -->">
 | 
			
		||||
  <meta name="og:type" content="website">
 | 
			
		||||
 | 
			
		||||
  <link rel="stylesheet" href="main.css" inline>
 | 
			
		||||
  <link rel="stylesheet" class="css-mode night" media="not" href="night.css" inline>
 | 
			
		||||
  <!-- inject:config -->
 | 
			
		||||
  <!-- contents of html partials will be injected here -->
 | 
			
		||||
  <!-- endinject -->
 | 
			
		||||
  <script src="vendor/polyfill.js" inline></script>
 | 
			
		||||
  <script src="vendor/promise.js" inline></script>
 | 
			
		||||
  <script src="vendor/promise/polyfill.js" inline></script>
 | 
			
		||||
  <script src="app.js"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div class="loader">
 | 
			
		||||
  <p>
 | 
			
		||||
    Lade<br />
 | 
			
		||||
    <img inline src="logo.svg" class="spinner" />
 | 
			
		||||
    <img inline src="logo.svg" class="spinner" alt="Loading ..."/>
 | 
			
		||||
    <br />
 | 
			
		||||
    Karten & Knoten...
 | 
			
		||||
  </p>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								html/offline.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,23 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <title><!-- inject:title --><!-- endinject --></title>
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
 | 
			
		||||
  <link rel="stylesheet" href="main.css" inline>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div class="loader">
 | 
			
		||||
  <p>
 | 
			
		||||
    You are Offline!<br />
 | 
			
		||||
    <img inline src="logo.svg" class="spinner" alt="Loading ..."/>
 | 
			
		||||
    <br />
 | 
			
		||||
    No connection available.
 | 
			
		||||
    <br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br />
 | 
			
		||||
  </p>
 | 
			
		||||
  <noscript>
 | 
			
		||||
    <strong>JavaScript required</strong>
 | 
			
		||||
  </noscript>
 | 
			
		||||
</div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										13
									
								
								lib/about.js
									
									
									
									
									
								
							
							
						
						@ -4,7 +4,18 @@ define(function () {
 | 
			
		||||
  return function () {
 | 
			
		||||
    this.render = function render(d) {
 | 
			
		||||
      d.innerHTML = _.t('sidebar.aboutInfo') +
 | 
			
		||||
 | 
			
		||||
        '<h4>' + _.t('node.nodes') + '</h4>' +
 | 
			
		||||
        '<p class="legend">' +
 | 
			
		||||
        '<span class="legend-new"><span class="symbol"></span> ' + _.t('sidebar.nodeNew') + '</span>' +
 | 
			
		||||
        '<span class="legend-online"><span class="symbol"></span> ' + _.t('sidebar.nodeOnline') + '</span>' +
 | 
			
		||||
        '<span class="legend-offline"><span class="symbol"></span> ' + _.t('sidebar.nodeOffline') + '</span>' +
 | 
			
		||||
        '</p>' +
 | 
			
		||||
        '<h4>' + _.t('node.clients') + '</h4>' +
 | 
			
		||||
        '<p class="legend">' +
 | 
			
		||||
        '<span class="legend-24ghz"><span class="symbol"></span> 2.4 GHz</span>' +
 | 
			
		||||
        '<span class="legend-5ghz"><span class="symbol"></span> 5 GHz</span>' +
 | 
			
		||||
        '<span class="legend-others"><span class="symbol"></span> ' + _.t('others') + '</span>' +
 | 
			
		||||
        '</p>' +
 | 
			
		||||
        '<h3>AGPL 3</h3>' +
 | 
			
		||||
 | 
			
		||||
        '<p>Copyright (C) Milan Pässler</p>' +
 | 
			
		||||
 | 
			
		||||
@ -97,8 +97,7 @@ define(['filters/nodefilter'], function (NodeFilter) {
 | 
			
		||||
      setData: setData,
 | 
			
		||||
      addFilter: addFilter,
 | 
			
		||||
      removeFilter: removeFilter,
 | 
			
		||||
      watchFilters: watchFilters,
 | 
			
		||||
      refresh: refresh
 | 
			
		||||
      watchFilters: watchFilters
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ define(function () {
 | 
			
		||||
 | 
			
		||||
        var button = document.createElement('button');
 | 
			
		||||
        button.classList.add('ion-close');
 | 
			
		||||
        button.setAttribute('aria-label', _.t('remove'));
 | 
			
		||||
        button.onclick = function onclick() {
 | 
			
		||||
          distributor.removeFilter(d);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ define(function () {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function run(d) {
 | 
			
		||||
      return (d.nodeinfo !== undefined ? d.nodeinfo.hostname.toLowerCase().includes(input.value.toLowerCase()) : '');
 | 
			
		||||
      return d.hostname.toLowerCase().includes(input.value.toLowerCase());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setRefresh(f) {
 | 
			
		||||
@ -26,6 +26,7 @@ define(function () {
 | 
			
		||||
    function render(el) {
 | 
			
		||||
      input.type = 'search';
 | 
			
		||||
      input.placeholder = _.t('sidebar.nodeFilter');
 | 
			
		||||
      input.setAttribute('aria-label', _.t('sidebar.nodeFilter'));
 | 
			
		||||
      input.addEventListener('input', refresh);
 | 
			
		||||
      el.classList.add('filter-node');
 | 
			
		||||
      el.classList.add('ion-filter');
 | 
			
		||||
 | 
			
		||||
@ -12,26 +12,8 @@ define(function () {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var filteredIds = new Set();
 | 
			
		||||
 | 
			
		||||
      n.graph = {};
 | 
			
		||||
      n.graph.nodes = data.graph.nodes.filter(function (d) {
 | 
			
		||||
        var r;
 | 
			
		||||
        if (d.node) {
 | 
			
		||||
          r = filter(d.node);
 | 
			
		||||
        } else {
 | 
			
		||||
          r = filter({});
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (r) {
 | 
			
		||||
          filteredIds.add(d.id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return r;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      n.graph.links = data.graph.links.filter(function (d) {
 | 
			
		||||
        return filteredIds.has(d.source.id) && filteredIds.has(d.target.id);
 | 
			
		||||
      n.links = data.links.filter(function (d) {
 | 
			
		||||
        return filter(d.source) && filter(d.target);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return n;
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegraph/draw'],
 | 
			
		||||
  function (d3Selection, d3Force, d3Zoom, d3Drag, math, draw) {
 | 
			
		||||
define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease', 'd3-interpolate', 'utils/math', 'forcegraph/draw'],
 | 
			
		||||
  function (d3Selection, d3Force, d3Zoom, d3Drag, d3Timer, d3Ease, d3Interpolate, math, draw) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
 | 
			
		||||
    return function (config, linkScale, sidebar, router) {
 | 
			
		||||
    return function (linkScale, sidebar) {
 | 
			
		||||
      var self = this;
 | 
			
		||||
      var el;
 | 
			
		||||
      var canvas;
 | 
			
		||||
@ -14,15 +14,18 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
      var intNodes = [];
 | 
			
		||||
      var dictNodes = {};
 | 
			
		||||
      var intLinks = [];
 | 
			
		||||
      var movetoTimer;
 | 
			
		||||
      var initial = 1.8;
 | 
			
		||||
 | 
			
		||||
      var NODE_RADIUS_DRAG = 10;
 | 
			
		||||
      var NODE_RADIUS_SELECT = 15;
 | 
			
		||||
      var LINK_RADIUS_SELECT = 12;
 | 
			
		||||
      var ZOOM_ANIMATE_DURATION = 350;
 | 
			
		||||
 | 
			
		||||
      var ZOOM_MIN = 1 / 8;
 | 
			
		||||
      var ZOOM_MAX = 3;
 | 
			
		||||
 | 
			
		||||
      var FORCE_ALPHA = 0.3;
 | 
			
		||||
      var FORCE_ALPHA = 0.01;
 | 
			
		||||
 | 
			
		||||
      draw.setTransform(transform);
 | 
			
		||||
 | 
			
		||||
@ -32,9 +35,43 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
        draw.setMaxArea(canvas.width, canvas.height);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function moveTo(x, y) {
 | 
			
		||||
        transform.x = (canvas.width + sidebar()) / 2 - x * transform.k;
 | 
			
		||||
        transform.y = canvas.height / 2 - y * transform.k;
 | 
			
		||||
      function transformPosition(p) {
 | 
			
		||||
        transform.x = p.x;
 | 
			
		||||
        transform.y = p.y;
 | 
			
		||||
        transform.k = p.k;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function moveTo(callback, forceMove) {
 | 
			
		||||
        clearTimeout(movetoTimer);
 | 
			
		||||
        if (!forceMove && force.alpha() > 0.3) {
 | 
			
		||||
          movetoTimer = setTimeout(function timerOfMoveTo() {
 | 
			
		||||
            moveTo(callback);
 | 
			
		||||
          }, 300);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        var result = callback();
 | 
			
		||||
        var x = result[0];
 | 
			
		||||
        var y = result[1];
 | 
			
		||||
        var k = result[2];
 | 
			
		||||
        var end = { k: k };
 | 
			
		||||
 | 
			
		||||
        end.x = (canvas.width + sidebar.getWidth()) / 2 - x * k;
 | 
			
		||||
        end.y = canvas.height / 2 - y * k;
 | 
			
		||||
 | 
			
		||||
        var start = { x: transform.x, y: transform.y, k: transform.k };
 | 
			
		||||
 | 
			
		||||
        var interpolate = d3Interpolate.interpolateObject(start, end);
 | 
			
		||||
 | 
			
		||||
        var timer = d3Timer.timer(function (t) {
 | 
			
		||||
          if (t >= ZOOM_ANIMATE_DURATION) {
 | 
			
		||||
            timer.stop();
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          var v = interpolate(d3Ease.easeQuadInOut(t / ZOOM_ANIMATE_DURATION));
 | 
			
		||||
          transformPosition(v);
 | 
			
		||||
          window.requestAnimationFrame(redraw);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function onClick() {
 | 
			
		||||
@ -46,7 +83,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
        var n = force.find(e[0], e[1], NODE_RADIUS_SELECT);
 | 
			
		||||
 | 
			
		||||
        if (n !== undefined) {
 | 
			
		||||
          router.fullUrl({ node: n.o.node.nodeinfo.node_id });
 | 
			
		||||
          router.fullUrl({ node: n.o.node_id });
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -85,16 +122,16 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
 | 
			
		||||
      forceLink = d3Force.forceLink()
 | 
			
		||||
        .distance(function (d) {
 | 
			
		||||
          if (d.o.vpn) {
 | 
			
		||||
          if (d.o.type.indexOf('vpn') === 0) {
 | 
			
		||||
            return 0;
 | 
			
		||||
          }
 | 
			
		||||
          return 75;
 | 
			
		||||
        })
 | 
			
		||||
        .strength(function (d) {
 | 
			
		||||
          if (d.o.vpn) {
 | 
			
		||||
          if (d.o.type.indexOf('vpn') === 0) {
 | 
			
		||||
            return 0.02;
 | 
			
		||||
          }
 | 
			
		||||
          return Math.max(0.5, 1 / d.o.tq);
 | 
			
		||||
          return Math.max(0.5, d.o.source_tq);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      var zoom = d3Zoom.zoom()
 | 
			
		||||
@ -111,7 +148,8 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
        .force('x', d3Force.forceX().strength(0.02))
 | 
			
		||||
        .force('y', d3Force.forceY().strength(0.02))
 | 
			
		||||
        .force('collide', d3Force.forceCollide())
 | 
			
		||||
        .on('tick', redraw);
 | 
			
		||||
        .on('tick', redraw)
 | 
			
		||||
        .alphaDecay(0.025);
 | 
			
		||||
 | 
			
		||||
      var drag = d3Drag.drag()
 | 
			
		||||
        .subject(function () {
 | 
			
		||||
@ -160,13 +198,11 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      self.setData = function setData(data) {
 | 
			
		||||
        intNodes = data.graph.nodes.map(function (d) {
 | 
			
		||||
          var e;
 | 
			
		||||
          if (d.id in dictNodes) {
 | 
			
		||||
            e = dictNodes[d.id];
 | 
			
		||||
          } else {
 | 
			
		||||
        intNodes = data.nodes.all.map(function (d) {
 | 
			
		||||
          var e = dictNodes[d.node_id];
 | 
			
		||||
          if (!e) {
 | 
			
		||||
            e = {};
 | 
			
		||||
            dictNodes[d.id] = e;
 | 
			
		||||
            dictNodes[d.node_id] = e;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          e.o = d;
 | 
			
		||||
@ -174,65 +210,67 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
 | 
			
		||||
          return e;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        intLinks = data.graph.links.map(function (d) {
 | 
			
		||||
          var e = {};
 | 
			
		||||
          e.o = d;
 | 
			
		||||
          e.source = dictNodes[d.source.id];
 | 
			
		||||
          e.target = dictNodes[d.target.id];
 | 
			
		||||
          e.color = linkScale(1 / d.tq);
 | 
			
		||||
 | 
			
		||||
          return e;
 | 
			
		||||
        intLinks = data.links.filter(function (d) {
 | 
			
		||||
          return data.nodeDict[d.source.node_id].is_online && data.nodeDict[d.target.node_id].is_online;
 | 
			
		||||
        }).map(function (d) {
 | 
			
		||||
          return {
 | 
			
		||||
            o: d,
 | 
			
		||||
            source: dictNodes[d.source.node_id],
 | 
			
		||||
            target: dictNodes[d.target.node_id],
 | 
			
		||||
            color: linkScale(d.source_tq),
 | 
			
		||||
            color_to: linkScale(d.target_tq)
 | 
			
		||||
          };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        force.nodes(intNodes);
 | 
			
		||||
        forceLink.links(intLinks);
 | 
			
		||||
 | 
			
		||||
        force.alpha(1).restart();
 | 
			
		||||
        force.alpha(initial).velocityDecay(0.15).restart();
 | 
			
		||||
        if (initial === 1.8) {
 | 
			
		||||
          initial = 0.5;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resizeCanvas();
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.resetView = function resetView() {
 | 
			
		||||
        draw.setHighlight(null);
 | 
			
		||||
        transform.k = (ZOOM_MIN + 1) / 2;
 | 
			
		||||
        moveTo(0, 0);
 | 
			
		||||
        redraw();
 | 
			
		||||
        moveTo(function calcToReset() {
 | 
			
		||||
          draw.setHighlight(null);
 | 
			
		||||
          return [0, 0, (ZOOM_MIN + config.forceGraph.zoomModifier) / 2];
 | 
			
		||||
        }, true);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.gotoNode = function gotoNode(d) {
 | 
			
		||||
        for (var i = 0; i < intNodes.length; i++) {
 | 
			
		||||
          var n = intNodes[i];
 | 
			
		||||
          if (n.o.node.nodeinfo.node_id !== d.nodeinfo.node_id) {
 | 
			
		||||
            continue;
 | 
			
		||||
        moveTo(function calcToNode() {
 | 
			
		||||
          draw.setHighlight({ type: 'node', id: d.node_id });
 | 
			
		||||
          var n = dictNodes[d.node_id];
 | 
			
		||||
          if (n) {
 | 
			
		||||
            return [n.x, n.y, (ZOOM_MAX + 1) / 2];
 | 
			
		||||
          }
 | 
			
		||||
          draw.setHighlight({ type: 'node', o: n.o.node });
 | 
			
		||||
          transform.k = (ZOOM_MAX + 1) / 2;
 | 
			
		||||
          moveTo(n.x, n.y);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        redraw();
 | 
			
		||||
          return self.resetView();
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.gotoLink = function gotoLink(d) {
 | 
			
		||||
        draw.setHighlight({ type: 'link', o: d });
 | 
			
		||||
        for (var i = 0; i < intLinks.length; i++) {
 | 
			
		||||
          var l = intLinks[i];
 | 
			
		||||
          if (l.o !== d) {
 | 
			
		||||
            continue;
 | 
			
		||||
        moveTo(function calcToLink() {
 | 
			
		||||
          draw.setHighlight({ type: 'link', id: d[0].id });
 | 
			
		||||
          var l = intLinks.find(function (link) {
 | 
			
		||||
            return link.o.id === d[0].id;
 | 
			
		||||
          });
 | 
			
		||||
          if (l) {
 | 
			
		||||
            return [(l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2, (ZOOM_MAX / 2) + ZOOM_MIN];
 | 
			
		||||
          }
 | 
			
		||||
          moveTo((l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        redraw();
 | 
			
		||||
          return self.resetView();
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      self.gotoLocation = function gotoLocation() {
 | 
			
		||||
        // ignore
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.destroy = function destroy() {
 | 
			
		||||
        force.stop();
 | 
			
		||||
        canvas.remove();
 | 
			
		||||
        canvas.parentNode.removeChild(canvas);
 | 
			
		||||
        force = null;
 | 
			
		||||
 | 
			
		||||
        if (el.parentNode) {
 | 
			
		||||
 | 
			
		||||
@ -4,50 +4,39 @@ define(['helper'], function (helper) {
 | 
			
		||||
  var ctx;
 | 
			
		||||
  var width;
 | 
			
		||||
  var height;
 | 
			
		||||
 | 
			
		||||
  var transform;
 | 
			
		||||
 | 
			
		||||
  var highlight;
 | 
			
		||||
 | 
			
		||||
  var nodeColor = '#fff';
 | 
			
		||||
  var clientColor = '#e6324b';
 | 
			
		||||
  var highlightColor = 'rgba(255, 255, 255, 0.2)';
 | 
			
		||||
 | 
			
		||||
  var labelColor = '#fff';
 | 
			
		||||
 | 
			
		||||
  var NODE_RADIUS = 15;
 | 
			
		||||
  var LINE_RADIUS = 12;
 | 
			
		||||
 | 
			
		||||
  function drawDetailNode(d) {
 | 
			
		||||
    if (transform.k > 1) {
 | 
			
		||||
      ctx.beginPath();
 | 
			
		||||
      helper.positionClients(ctx, d, Math.PI, d.o.node.statistics.clients, 15);
 | 
			
		||||
      ctx.fillStyle = clientColor;
 | 
			
		||||
      ctx.fill();
 | 
			
		||||
    if (transform.k > 1 && d.o.is_online) {
 | 
			
		||||
      helper.positionClients(ctx, d, Math.PI, d.o, 15);
 | 
			
		||||
      ctx.beginPath();
 | 
			
		||||
      var name = d.o.node_id;
 | 
			
		||||
      if (d.o.node && d.o.node.nodeinfo) {
 | 
			
		||||
        name = d.o.node.nodeinfo.hostname;
 | 
			
		||||
      if (d.o) {
 | 
			
		||||
        name = d.o.hostname;
 | 
			
		||||
      }
 | 
			
		||||
      ctx.textAlign = 'center';
 | 
			
		||||
      ctx.fillStyle = labelColor;
 | 
			
		||||
      ctx.fillStyle = config.forceGraph.labelColor;
 | 
			
		||||
      ctx.fillText(name, d.x, d.y + 20);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function drawHighlightNode(d) {
 | 
			
		||||
    if (highlight && highlight.type === 'node' && d.o.node === highlight.o) {
 | 
			
		||||
    if (highlight && highlight.type === 'node' && d.o.node_id === highlight.id) {
 | 
			
		||||
      ctx.arc(d.x, d.y, NODE_RADIUS * 1.5, 0, 2 * Math.PI);
 | 
			
		||||
      ctx.fillStyle = highlightColor;
 | 
			
		||||
      ctx.fillStyle = config.forceGraph.highlightColor;
 | 
			
		||||
      ctx.fill();
 | 
			
		||||
      ctx.beginPath();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function drawHighlightLink(d, to) {
 | 
			
		||||
    if (highlight && highlight.type === 'link' && d.o === highlight.o) {
 | 
			
		||||
    if (highlight && highlight.type === 'link' && d.o.id === highlight.id) {
 | 
			
		||||
      ctx.lineTo(to[0], to[1]);
 | 
			
		||||
      ctx.strokeStyle = highlightColor;
 | 
			
		||||
      ctx.strokeStyle = config.forceGraph.highlightColor;
 | 
			
		||||
      ctx.lineWidth = LINE_RADIUS * 2;
 | 
			
		||||
      ctx.lineCap = 'round';
 | 
			
		||||
      ctx.stroke();
 | 
			
		||||
@ -64,9 +53,17 @@ define(['helper'], function (helper) {
 | 
			
		||||
 | 
			
		||||
    drawHighlightNode(d);
 | 
			
		||||
 | 
			
		||||
    ctx.moveTo(d.x + 3, d.y);
 | 
			
		||||
    ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI);
 | 
			
		||||
    ctx.fillStyle = nodeColor;
 | 
			
		||||
    if (d.o.is_online) {
 | 
			
		||||
      ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI);
 | 
			
		||||
      if (d.o.is_gateway) {
 | 
			
		||||
        ctx.rect(d.x - 9, d.y - 9, 18, 18);
 | 
			
		||||
      }
 | 
			
		||||
      ctx.fillStyle = config.forceGraph.nodeColor;
 | 
			
		||||
    } else {
 | 
			
		||||
      ctx.arc(d.x, d.y, 6, 0, 2 * Math.PI);
 | 
			
		||||
      ctx.fillStyle = config.forceGraph.nodeOfflineColor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ctx.fill();
 | 
			
		||||
 | 
			
		||||
    drawDetailNode(d);
 | 
			
		||||
@ -76,7 +73,7 @@ define(['helper'], function (helper) {
 | 
			
		||||
    var zero = transform.invert([0, 0]);
 | 
			
		||||
    var area = transform.invert([width, height]);
 | 
			
		||||
    if (d.source.x < zero[0] && d.target.x < zero[0] || d.source.y < zero[1] && d.target.y < zero[1] ||
 | 
			
		||||
        d.source.x > area[0] && d.target.x > area[0] || d.source.y > area[1] && d.target.y > area[1]) {
 | 
			
		||||
      d.source.x > area[0] && d.target.x > area[0] || d.source.y > area[1] && d.target.y > area[1]) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ctx.beginPath();
 | 
			
		||||
@ -85,9 +82,13 @@ define(['helper'], function (helper) {
 | 
			
		||||
 | 
			
		||||
    to = drawHighlightLink(d, to);
 | 
			
		||||
 | 
			
		||||
    var grd = ctx.createLinearGradient(d.source.x, d.source.y, d.target.x, d.target.y);
 | 
			
		||||
    grd.addColorStop(0.45, d.color);
 | 
			
		||||
    grd.addColorStop(0.55, d.color_to);
 | 
			
		||||
 | 
			
		||||
    ctx.lineTo(to[0], to[1]);
 | 
			
		||||
    ctx.strokeStyle = d.color;
 | 
			
		||||
    if (d.o.vpn) {
 | 
			
		||||
    ctx.strokeStyle = grd;
 | 
			
		||||
    if (d.o.type.indexOf('vpn') === 0) {
 | 
			
		||||
      ctx.globalAlpha = 0.2;
 | 
			
		||||
      ctx.lineWidth = 1.5;
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										216
									
								
								lib/gui.js
									
									
									
									
									
								
							
							
						
						@ -1,131 +1,143 @@
 | 
			
		||||
define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
 | 
			
		||||
  'linklist', 'nodelist', 'simplenodelist', 'infobox/main',
 | 
			
		||||
  'proportions', 'forcegraph', 'title', 'about', 'datadistributor',
 | 
			
		||||
  'filters/filtergui', 'filters/hostname'],
 | 
			
		||||
  function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
 | 
			
		||||
            Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
 | 
			
		||||
            Title, About, DataDistributor, FilterGUI, HostnameFilter) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
  'filters/filtergui', 'filters/hostname', 'helper'],
 | 
			
		||||
function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
 | 
			
		||||
  Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
 | 
			
		||||
  Title, About, DataDistributor, FilterGUI, HostnameFilter, helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
    return function (config, router, language) {
 | 
			
		||||
      var self = this;
 | 
			
		||||
      var content;
 | 
			
		||||
      var contentDiv;
 | 
			
		||||
  return function (language) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    var content;
 | 
			
		||||
    var contentDiv;
 | 
			
		||||
 | 
			
		||||
      var linkScale = d3Interpolate.interpolate('#F02311', '#04C714');
 | 
			
		||||
      var sidebar;
 | 
			
		||||
    var linkScale = d3Interpolate.interpolate(config.map.tqFrom, config.map.tqTo);
 | 
			
		||||
    var sidebar;
 | 
			
		||||
 | 
			
		||||
      var buttons = document.createElement('div');
 | 
			
		||||
      buttons.classList.add('buttons');
 | 
			
		||||
    var buttons = document.createElement('div');
 | 
			
		||||
    buttons.classList.add('buttons');
 | 
			
		||||
 | 
			
		||||
      var fanout = new DataDistributor();
 | 
			
		||||
      var fanoutUnfiltered = new DataDistributor();
 | 
			
		||||
      fanoutUnfiltered.add(fanout);
 | 
			
		||||
    var fanout = new DataDistributor();
 | 
			
		||||
    var fanoutUnfiltered = new DataDistributor();
 | 
			
		||||
    fanoutUnfiltered.add(fanout);
 | 
			
		||||
 | 
			
		||||
      function removeContent() {
 | 
			
		||||
        if (!content) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        router.removeTarget(content);
 | 
			
		||||
        fanout.remove(content);
 | 
			
		||||
 | 
			
		||||
        content.destroy();
 | 
			
		||||
 | 
			
		||||
        content = null;
 | 
			
		||||
    function removeContent() {
 | 
			
		||||
      if (!content) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function addContent(K) {
 | 
			
		||||
        removeContent();
 | 
			
		||||
      router.removeTarget(content);
 | 
			
		||||
      fanout.remove(content);
 | 
			
		||||
 | 
			
		||||
        content = new K(config, linkScale, sidebar.getWidth, router, buttons);
 | 
			
		||||
        content.render(contentDiv);
 | 
			
		||||
      content.destroy();
 | 
			
		||||
 | 
			
		||||
        fanout.add(content);
 | 
			
		||||
        router.addTarget(content);
 | 
			
		||||
      content = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addContent(K) {
 | 
			
		||||
      removeContent();
 | 
			
		||||
 | 
			
		||||
      content = new K(linkScale, sidebar, buttons);
 | 
			
		||||
      content.render(contentDiv);
 | 
			
		||||
 | 
			
		||||
      fanout.add(content);
 | 
			
		||||
      router.addTarget(content);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function mkView(K) {
 | 
			
		||||
      return function () {
 | 
			
		||||
        addContent(K);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var loader = document.getElementsByClassName('loader')[0];
 | 
			
		||||
    loader.classList.add('hide');
 | 
			
		||||
 | 
			
		||||
    contentDiv = document.createElement('div');
 | 
			
		||||
    contentDiv.classList.add('content');
 | 
			
		||||
    document.body.appendChild(contentDiv);
 | 
			
		||||
 | 
			
		||||
    sidebar = new Sidebar(document.body);
 | 
			
		||||
 | 
			
		||||
    contentDiv.appendChild(buttons);
 | 
			
		||||
 | 
			
		||||
    var buttonToggle = document.createElement('button');
 | 
			
		||||
    buttonToggle.classList.add('ion-eye');
 | 
			
		||||
    buttonToggle.setAttribute('aria-label', _.t('button.switchView'));
 | 
			
		||||
    buttonToggle.onclick = function onclick() {
 | 
			
		||||
      var data;
 | 
			
		||||
      if (content.constructor === Map) {
 | 
			
		||||
        data = { view: 'graph', lat: undefined, lng: undefined, zoom: undefined };
 | 
			
		||||
      } else {
 | 
			
		||||
        data = { view: 'map' };
 | 
			
		||||
      }
 | 
			
		||||
      router.fullUrl(data, false, true);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
      function mkView(K) {
 | 
			
		||||
        return function () {
 | 
			
		||||
          addContent(K);
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    buttons.appendChild(buttonToggle);
 | 
			
		||||
 | 
			
		||||
      var loader = document.getElementsByClassName('loader')[0];
 | 
			
		||||
      loader.classList.add('hide');
 | 
			
		||||
 | 
			
		||||
      contentDiv = document.createElement('div');
 | 
			
		||||
      contentDiv.classList.add('content');
 | 
			
		||||
      document.body.appendChild(contentDiv);
 | 
			
		||||
 | 
			
		||||
      sidebar = new Sidebar(document.body);
 | 
			
		||||
 | 
			
		||||
      contentDiv.appendChild(buttons);
 | 
			
		||||
 | 
			
		||||
      var buttonToggle = document.createElement('button');
 | 
			
		||||
      buttonToggle.classList.add('ion-eye', 'shadow');
 | 
			
		||||
      buttonToggle.setAttribute('data-tooltip', _.t('button.switchView'));
 | 
			
		||||
      buttonToggle.onclick = function onclick() {
 | 
			
		||||
        var data;
 | 
			
		||||
        if (content.constructor === Map) {
 | 
			
		||||
          data = { view: 'graph', lat: undefined, lng: undefined, zoom: undefined };
 | 
			
		||||
        } else {
 | 
			
		||||
          data = { view: 'map' };
 | 
			
		||||
        }
 | 
			
		||||
        router.fullUrl(data, false, true);
 | 
			
		||||
    if (config.fullscreen || config.fullscreenFrame && window.frameElement) {
 | 
			
		||||
      var buttonFullscreen = document.createElement('button');
 | 
			
		||||
      buttonFullscreen.classList.add('ion-full-enter');
 | 
			
		||||
      buttonFullscreen.setAttribute('aria-label', _.t('button.fullscreen'));
 | 
			
		||||
      buttonFullscreen.onclick = function onclick() {
 | 
			
		||||
        helper.fullscreen(buttonFullscreen);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      buttons.appendChild(buttonToggle);
 | 
			
		||||
      buttons.appendChild(buttonFullscreen);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      var title = new Title(config);
 | 
			
		||||
    var title = new Title();
 | 
			
		||||
 | 
			
		||||
      var header = new Container('header');
 | 
			
		||||
      var infobox = new Infobox(config, sidebar, router);
 | 
			
		||||
      var tabs = new Tabs();
 | 
			
		||||
      var overview = new Container();
 | 
			
		||||
      var legend = new Legend(config, language);
 | 
			
		||||
      var newnodeslist = new SimpleNodelist('new', 'firstseen', router, _.t('node.new'));
 | 
			
		||||
      var lostnodeslist = new SimpleNodelist('lost', 'lastseen', router, _.t('node.missing'));
 | 
			
		||||
      var nodelist = new Nodelist(router);
 | 
			
		||||
      var linklist = new Linklist(linkScale, router);
 | 
			
		||||
      var statistics = new Proportions(config, fanout);
 | 
			
		||||
      var about = new About();
 | 
			
		||||
    var header = new Container('header');
 | 
			
		||||
    var infobox = new Infobox(sidebar, linkScale);
 | 
			
		||||
    var tabs = new Tabs();
 | 
			
		||||
    var overview = new Container();
 | 
			
		||||
    var legend = new Legend(language);
 | 
			
		||||
    var newnodeslist = new SimpleNodelist('new', 'firstseen', _.t('node.new'));
 | 
			
		||||
    var lostnodeslist = new SimpleNodelist('lost', 'lastseen',  _.t('node.missing'));
 | 
			
		||||
    var nodelist = new Nodelist();
 | 
			
		||||
    var linklist = new Linklist(linkScale);
 | 
			
		||||
    var statistics = new Proportions(fanout);
 | 
			
		||||
    var about = new About();
 | 
			
		||||
 | 
			
		||||
      fanoutUnfiltered.add(legend);
 | 
			
		||||
      fanoutUnfiltered.add(newnodeslist);
 | 
			
		||||
      fanoutUnfiltered.add(lostnodeslist);
 | 
			
		||||
      fanout.add(nodelist);
 | 
			
		||||
      fanout.add(linklist);
 | 
			
		||||
      fanout.add(statistics);
 | 
			
		||||
    fanoutUnfiltered.add(legend);
 | 
			
		||||
    fanoutUnfiltered.add(newnodeslist);
 | 
			
		||||
    fanoutUnfiltered.add(lostnodeslist);
 | 
			
		||||
    fanoutUnfiltered.add(infobox);
 | 
			
		||||
    fanout.add(nodelist);
 | 
			
		||||
    fanout.add(linklist);
 | 
			
		||||
    fanout.add(statistics);
 | 
			
		||||
 | 
			
		||||
      sidebar.add(header);
 | 
			
		||||
      header.add(legend);
 | 
			
		||||
    sidebar.add(header);
 | 
			
		||||
    header.add(legend);
 | 
			
		||||
 | 
			
		||||
      overview.add(newnodeslist);
 | 
			
		||||
      overview.add(lostnodeslist);
 | 
			
		||||
    overview.add(newnodeslist);
 | 
			
		||||
    overview.add(lostnodeslist);
 | 
			
		||||
 | 
			
		||||
      var filterGUI = new FilterGUI(fanout);
 | 
			
		||||
      fanout.watchFilters(filterGUI);
 | 
			
		||||
      header.add(filterGUI);
 | 
			
		||||
    var filterGUI = new FilterGUI(fanout);
 | 
			
		||||
    fanout.watchFilters(filterGUI);
 | 
			
		||||
    header.add(filterGUI);
 | 
			
		||||
 | 
			
		||||
      var hostnameFilter = new HostnameFilter();
 | 
			
		||||
      fanout.addFilter(hostnameFilter);
 | 
			
		||||
    var hostnameFilter = new HostnameFilter();
 | 
			
		||||
    fanout.addFilter(hostnameFilter);
 | 
			
		||||
 | 
			
		||||
      sidebar.add(tabs);
 | 
			
		||||
      tabs.add('sidebar.actual', overview);
 | 
			
		||||
      tabs.add('node.nodes', nodelist);
 | 
			
		||||
      tabs.add('node.links', linklist);
 | 
			
		||||
      tabs.add('sidebar.stats', statistics);
 | 
			
		||||
      tabs.add('sidebar.about', about);
 | 
			
		||||
    sidebar.add(tabs);
 | 
			
		||||
    tabs.add('sidebar.actual', overview);
 | 
			
		||||
    tabs.add('node.nodes', nodelist);
 | 
			
		||||
    tabs.add('node.links', linklist);
 | 
			
		||||
    tabs.add('sidebar.stats', statistics);
 | 
			
		||||
    tabs.add('sidebar.about', about);
 | 
			
		||||
 | 
			
		||||
      router.addTarget(title);
 | 
			
		||||
      router.addTarget(infobox);
 | 
			
		||||
    router.addTarget(title);
 | 
			
		||||
    router.addTarget(infobox);
 | 
			
		||||
 | 
			
		||||
      router.addView('map', mkView(Map));
 | 
			
		||||
      router.addView('graph', mkView(ForceGraph));
 | 
			
		||||
    router.addView('map', mkView(Map));
 | 
			
		||||
    router.addView('graph', mkView(ForceGraph));
 | 
			
		||||
 | 
			
		||||
      self.setData = fanoutUnfiltered.setData;
 | 
			
		||||
    self.setData = fanoutUnfiltered.setData;
 | 
			
		||||
 | 
			
		||||
      return self;
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
    return self;
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,53 +1,89 @@
 | 
			
		||||
define(['helper'], function (helper) {
 | 
			
		||||
define(['helper', 'snabbdom'], function (helper, V) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  V = V.default;
 | 
			
		||||
 | 
			
		||||
  function showStatImg(o, d, time) {
 | 
			
		||||
    var subst = {};
 | 
			
		||||
    subst['{SOURCE_ID}'] = d.source.node_id;
 | 
			
		||||
    subst['{SOURCE_NAME}'] = d.source.node.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
 | 
			
		||||
    subst['{TARGET_ID}'] = d.target.node_id;
 | 
			
		||||
    subst['{TARGET_NAME}'] = d.target.node.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
 | 
			
		||||
    subst['{TIME}'] = time;
 | 
			
		||||
    subst['{LOCALE}'] = _.locale();
 | 
			
		||||
    return helper.showStat(o, subst);
 | 
			
		||||
  function showStatImg(img, o, d, time) {
 | 
			
		||||
    var subst = {
 | 
			
		||||
      '{SOURCE_ID}': d.source.node_id,
 | 
			
		||||
      '{SOURCE_NAME}': d.source.hostname.replace(/[^a-z0-9\-]/ig, '_'),
 | 
			
		||||
      '{SOURCE_ADDR}': d.source_addr,
 | 
			
		||||
      '{SOURCE_MAC}': d.source_mac ? d.source_mac : d.source_addr,
 | 
			
		||||
      '{TARGET_ID}': d.target.node_id,
 | 
			
		||||
      '{TARGET_NAME}': d.target.hostname.replace(/[^a-z0-9\-]/ig, '_'),
 | 
			
		||||
      '{TARGET_ADDR}': d.target_addr,
 | 
			
		||||
      '{TARGET_MAC}': d.target_mac ? d.target_mac : d.target_addr,
 | 
			
		||||
      '{TYPE}': d.type,
 | 
			
		||||
      '{TIME}': time,
 | 
			
		||||
      '{LOCALE}': _.locale()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    img.push(V.h('h4', helper.listReplace(o.name, subst)));
 | 
			
		||||
    img.push(helper.showStat(V, o, subst));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return function (config, el, router, d) {
 | 
			
		||||
    var h2 = document.createElement('h2');
 | 
			
		||||
    var a1 = document.createElement('a');
 | 
			
		||||
    a1.href = router.generateLink({ node: d.source.node_id });
 | 
			
		||||
    a1.textContent = d.source.node.nodeinfo.hostname;
 | 
			
		||||
    h2.appendChild(a1);
 | 
			
		||||
  return function (el, d, linkScale) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    var header = document.createElement('div');
 | 
			
		||||
    var table = document.createElement('table');
 | 
			
		||||
    var images = document.createElement('div');
 | 
			
		||||
    el.appendChild(header);
 | 
			
		||||
    el.appendChild(table);
 | 
			
		||||
    el.appendChild(images);
 | 
			
		||||
 | 
			
		||||
    var arrow = document.createElement('span');
 | 
			
		||||
    arrow.classList.add('ion-arrow-right-c');
 | 
			
		||||
    h2.appendChild(arrow);
 | 
			
		||||
    self.render = function render() {
 | 
			
		||||
      var children = [];
 | 
			
		||||
      var img = [];
 | 
			
		||||
      var time = d[0].target.lastseen.format('DDMMYYYYHmmss');
 | 
			
		||||
 | 
			
		||||
    var a2 = document.createElement('a');
 | 
			
		||||
    a2.href = router.generateLink({ node: d.target.node_id });
 | 
			
		||||
    a2.textContent = d.target.node.nodeinfo.hostname;
 | 
			
		||||
    h2.appendChild(a2);
 | 
			
		||||
    el.appendChild(h2);
 | 
			
		||||
      header = V.patch(header, V.h('div', V.h('h2', [
 | 
			
		||||
        V.h('a', {
 | 
			
		||||
          props: { href: router.generateLink({ node: d[0].source.node_id }) }
 | 
			
		||||
        }, d[0].source.hostname),
 | 
			
		||||
        V.h('span', ' - '),
 | 
			
		||||
        V.h('a', {
 | 
			
		||||
          props: { href: router.generateLink({ node: d[0].target.node_id }) }
 | 
			
		||||
        }, d[0].target.hostname)
 | 
			
		||||
      ])));
 | 
			
		||||
 | 
			
		||||
    var attributes = document.createElement('table');
 | 
			
		||||
    attributes.classList.add('attributes');
 | 
			
		||||
      helper.attributeEntry(V, children, 'node.hardware', (d[0].source.model ? d[0].source.model + ' – ' : '') +
 | 
			
		||||
        (d[0].target.model ? d[0].target.model : ''));
 | 
			
		||||
      helper.attributeEntry(V, children, 'node.distance', helper.showDistance(d[0]));
 | 
			
		||||
 | 
			
		||||
    helper.attributeEntry(attributes, 'node.tq', helper.showTq(d));
 | 
			
		||||
    helper.attributeEntry(attributes, 'node.distance', helper.showDistance(d));
 | 
			
		||||
    var hw1 = helper.dictGet(d.source.node.nodeinfo, ['hardware', 'model']);
 | 
			
		||||
    var hw2 = helper.dictGet(d.target.node.nodeinfo, ['hardware', 'model']);
 | 
			
		||||
    helper.attributeEntry(attributes, 'node.hardware', hw1 + ' – ' + hw2);
 | 
			
		||||
      d.forEach(function (link) {
 | 
			
		||||
        children.push(V.h('tr', { props: { className: 'header' } }, [
 | 
			
		||||
          V.h('th', _.t('node.connectionType')),
 | 
			
		||||
          V.h('th', link.type)
 | 
			
		||||
        ]));
 | 
			
		||||
        helper.attributeEntry(V, children, 'node.tq', V.h('span',
 | 
			
		||||
          { style: { color: linkScale((link.source_tq + link.target_tq) / 2) } },
 | 
			
		||||
          helper.showTq(link.source_tq) + ' - ' + helper.showTq(link.target_tq))
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    el.appendChild(attributes);
 | 
			
		||||
 | 
			
		||||
    if (config.linkInfos) {
 | 
			
		||||
      var time = d.target.node.lastseen.format('DDMMYYYYHmmss');
 | 
			
		||||
      config.linkInfos.forEach(function (linkInfo) {
 | 
			
		||||
        var h4 = document.createElement('h4');
 | 
			
		||||
        h4.textContent = linkInfo.name;
 | 
			
		||||
        el.appendChild(h4);
 | 
			
		||||
        el.appendChild(showStatImg(linkInfo, d, time));
 | 
			
		||||
        if (config.linkTypeInfos) {
 | 
			
		||||
          config.linkTypeInfos.forEach(function (o) {
 | 
			
		||||
            showStatImg(img, o, link, time);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      if (config.linkInfos) {
 | 
			
		||||
        config.linkInfos.forEach(function (o) {
 | 
			
		||||
          showStatImg(img, o, d[0], time);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var elNew = V.h('table', children);
 | 
			
		||||
      table = V.patch(table, elNew);
 | 
			
		||||
      table.elm.classList.add('attributes');
 | 
			
		||||
      images = V.patch(images, V.h('div', img));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.setData = function setData(data) {
 | 
			
		||||
      d = data.links.filter(function (a) {
 | 
			
		||||
        return a.id === d[0].id;
 | 
			
		||||
      });
 | 
			
		||||
      self.render();
 | 
			
		||||
    };
 | 
			
		||||
    return self;
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
define(['helper'], function (helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  return function (config, el, router, d) {
 | 
			
		||||
  return function (el, d) {
 | 
			
		||||
    var sidebarTitle = document.createElement('h2');
 | 
			
		||||
    sidebarTitle.textContent = _.t('location.location');
 | 
			
		||||
    el.appendChild(sidebarTitle);
 | 
			
		||||
@ -14,16 +14,19 @@ define(['helper'], function (helper) {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    var editLat = document.createElement('input');
 | 
			
		||||
    editLat.setAttribute('aria-label', _.t('location.latitude'));
 | 
			
		||||
    editLat.type = 'text';
 | 
			
		||||
    editLat.value = d.lat.toFixed(9);
 | 
			
		||||
    el.appendChild(createBox('lat', _.t('location.latitude'), editLat));
 | 
			
		||||
 | 
			
		||||
    var editLng = document.createElement('input');
 | 
			
		||||
    editLng.setAttribute('aria-label', _.t('location.longitude'));
 | 
			
		||||
    editLng.type = 'text';
 | 
			
		||||
    editLng.value = d.lng.toFixed(9);
 | 
			
		||||
    el.appendChild(createBox('lng', _.t('location.longitude'), editLng));
 | 
			
		||||
 | 
			
		||||
    var editUci = document.createElement('textarea');
 | 
			
		||||
    editUci.setAttribute('aria-label', 'Uci');
 | 
			
		||||
    editUci.value =
 | 
			
		||||
      "uci set gluon-node-info.@location[0]='location'; " +
 | 
			
		||||
      "uci set gluon-node-info.@location[0].share_location='1';" +
 | 
			
		||||
@ -41,6 +44,7 @@ define(['helper'], function (helper) {
 | 
			
		||||
      var btn = document.createElement('button');
 | 
			
		||||
      btn.classList.add('ion-clipboard');
 | 
			
		||||
      btn.title = _.t('location.copy');
 | 
			
		||||
      btn.setAttribute('aria-label', _.t('location.copy'));
 | 
			
		||||
      btn.onclick = function onclick() {
 | 
			
		||||
        copy2clip(inputElem.id);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,16 @@
 | 
			
		||||
define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, node, location) {
 | 
			
		||||
define(['infobox/link', 'infobox/node', 'infobox/location'], function (Link, Node, location) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  return function (config, sidebar, router) {
 | 
			
		||||
  return function (sidebar, linkScale) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    var el;
 | 
			
		||||
    var node;
 | 
			
		||||
    var link;
 | 
			
		||||
 | 
			
		||||
    function destroy() {
 | 
			
		||||
      if (el && el.parentNode) {
 | 
			
		||||
        el.parentNode.removeChild(el);
 | 
			
		||||
        el = undefined;
 | 
			
		||||
        node = link = el = undefined;
 | 
			
		||||
        sidebar.reveal();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -28,6 +30,7 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod
 | 
			
		||||
      var closeButton = document.createElement('button');
 | 
			
		||||
      closeButton.classList.add('close');
 | 
			
		||||
      closeButton.classList.add('ion-close');
 | 
			
		||||
      closeButton.setAttribute('aria-label', _.t('close'));
 | 
			
		||||
      closeButton.onclick = function () {
 | 
			
		||||
        router.fullUrl();
 | 
			
		||||
      };
 | 
			
		||||
@ -36,19 +39,30 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod
 | 
			
		||||
 | 
			
		||||
    self.resetView = destroy;
 | 
			
		||||
 | 
			
		||||
    self.gotoNode = function gotoNode(d) {
 | 
			
		||||
    self.gotoNode = function gotoNode(d, nodeDict) {
 | 
			
		||||
      create();
 | 
			
		||||
      node(config, el, router, d);
 | 
			
		||||
      node = new Node(el, d, linkScale, nodeDict);
 | 
			
		||||
      node.render();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.gotoLink = function gotoLink(d) {
 | 
			
		||||
      create();
 | 
			
		||||
      link(config, el, router, d);
 | 
			
		||||
      link = new Link(el, d, linkScale);
 | 
			
		||||
      link.render();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.gotoLocation = function gotoLocation(d) {
 | 
			
		||||
      create();
 | 
			
		||||
      location(config, el, router, d);
 | 
			
		||||
      location(el, d);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.setData = function setData(d) {
 | 
			
		||||
      if (typeof node === 'object') {
 | 
			
		||||
        node.setData(d);
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof link === 'object') {
 | 
			
		||||
        link.setData(d);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return self;
 | 
			
		||||
 | 
			
		||||
@ -1,297 +1,194 @@
 | 
			
		||||
define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
 | 
			
		||||
  function (SortTable, V, d3Interpolate, moment, helper) {
 | 
			
		||||
define(['sorttable', 'snabbdom', 'd3-interpolate', 'helper', 'utils/node'],
 | 
			
		||||
  function (SortTable, V, d3Interpolate, helper, nodef) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
    V = V.default;
 | 
			
		||||
 | 
			
		||||
    function showGeoURI(d) {
 | 
			
		||||
      if (!helper.hasLocation(d)) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        var a = document.createElement('a');
 | 
			
		||||
        a.textContent = Number(d.nodeinfo.location.latitude.toFixed(6)) + ', ' + Number(d.nodeinfo.location.longitude.toFixed(6));
 | 
			
		||||
        a.href = 'geo:' + d.nodeinfo.location.latitude + ',' + d.nodeinfo.location.longitude;
 | 
			
		||||
        el.appendChild(a);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showStatus(d) {
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        el.classList.add(d.flags.unseen ? 'unseen' : (d.flags.online ? 'online' : 'offline'));
 | 
			
		||||
        el.textContent = _.t((d.flags.online ? 'node.lastOnline' : 'node.lastOffline'), {
 | 
			
		||||
          time: d.lastseen.fromNow(),
 | 
			
		||||
          date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showFirmware(d) {
 | 
			
		||||
      var release = helper.dictGet(d.nodeinfo, ['software', 'firmware', 'release']);
 | 
			
		||||
      var base = helper.dictGet(d.nodeinfo, ['software', 'firmware', 'base']);
 | 
			
		||||
 | 
			
		||||
      if (release === null || base === null) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return release + ' / ' + base;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showSite(d, config) {
 | 
			
		||||
      var site = helper.dictGet(d.nodeinfo, ['system', 'site_code']);
 | 
			
		||||
      var rt = site;
 | 
			
		||||
      if (config.siteNames) {
 | 
			
		||||
        config.siteNames.forEach(function (t) {
 | 
			
		||||
          if (site === t.site) {
 | 
			
		||||
            rt = t.name;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return rt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showUptime(d) {
 | 
			
		||||
      if (!('uptime' in d.statistics)) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return moment.duration(d.statistics.uptime, 'seconds').humanize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showFirstseen(d) {
 | 
			
		||||
      if (!('firstseen' in d)) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return d.firstseen.fromNow(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showClients(d) {
 | 
			
		||||
      if (!d.flags.online) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : _.t('none')));
 | 
			
		||||
        el.appendChild(document.createElement('br'));
 | 
			
		||||
 | 
			
		||||
        var span = document.createElement('span');
 | 
			
		||||
        span.classList.add('clients');
 | 
			
		||||
        span.innerHTML = '<i class="ion-person"></i>'.repeat(d.statistics.clients);
 | 
			
		||||
        el.appendChild(span);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showIPs(d) {
 | 
			
		||||
      var ips = helper.dictGet(d.nodeinfo, ['network', 'addresses']);
 | 
			
		||||
      if (ips === null) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ips.sort();
 | 
			
		||||
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        ips.forEach(function (ip, i) {
 | 
			
		||||
          var link = !ip.startsWith('fe80:');
 | 
			
		||||
 | 
			
		||||
          if (i > 0) {
 | 
			
		||||
            el.appendChild(document.createElement('br'));
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (link) {
 | 
			
		||||
            var a = document.createElement('a');
 | 
			
		||||
            a.href = 'http://[' + ip + ']/';
 | 
			
		||||
            a.textContent = ip;
 | 
			
		||||
            el.appendChild(a);
 | 
			
		||||
          } else {
 | 
			
		||||
            el.appendChild(document.createTextNode(ip));
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showBar(v, width, warning) {
 | 
			
		||||
      var span = document.createElement('span');
 | 
			
		||||
      span.classList.add('bar');
 | 
			
		||||
 | 
			
		||||
      var bar = document.createElement('span');
 | 
			
		||||
      bar.style.width = (width * 100) + '%';
 | 
			
		||||
      if (warning) {
 | 
			
		||||
        span.classList.add('warning');
 | 
			
		||||
      }
 | 
			
		||||
      span.appendChild(bar);
 | 
			
		||||
 | 
			
		||||
      var label = document.createElement('label');
 | 
			
		||||
      label.textContent = v;
 | 
			
		||||
      span.appendChild(label);
 | 
			
		||||
 | 
			
		||||
      return span;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showLoad(d) {
 | 
			
		||||
      if (!('loadavg' in d.statistics)) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        var value = d.statistics.loadavg.toFixed(2);
 | 
			
		||||
        var width = d.statistics.loadavg % 1;
 | 
			
		||||
        var warning = false;
 | 
			
		||||
        if (d.statistics.loadavg >= d.nodeinfo.hardware.nproc) {
 | 
			
		||||
          warning = true;
 | 
			
		||||
        }
 | 
			
		||||
        el.appendChild(showBar(value, width, warning));
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showRAM(d) {
 | 
			
		||||
      if (!('memory_usage' in d.statistics)) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return function (el) {
 | 
			
		||||
        var value = Math.round(d.statistics.memory_usage * 100) + ' %';
 | 
			
		||||
        var width = d.statistics.memory_usage;
 | 
			
		||||
        var warning = false;
 | 
			
		||||
        if (d.statistics.memory_usage >= 0.8) {
 | 
			
		||||
          warning = true;
 | 
			
		||||
        }
 | 
			
		||||
        el.appendChild(showBar(value, width, warning));
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showAutoupdate(d) {
 | 
			
		||||
      var au = helper.dictGet(d.nodeinfo, ['software', 'autoupdater']);
 | 
			
		||||
      if (!au) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return au.enabled ? _.t('node.activated', { branch: au.branch }) : _.t('node.deactivated');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showStatImg(o, d) {
 | 
			
		||||
      var subst = {};
 | 
			
		||||
      subst['{NODE_ID}'] = d.nodeinfo.node_id;
 | 
			
		||||
      subst['{NODE_NAME}'] = d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
 | 
			
		||||
      subst['{TIME}'] = d.lastseen.format('DDMMYYYYHmmss');
 | 
			
		||||
      subst['{LOCALE}'] = _.locale();
 | 
			
		||||
      return helper.showStat(o, subst);
 | 
			
		||||
      var subst = {
 | 
			
		||||
        '{NODE_ID}': d.node_id,
 | 
			
		||||
        '{NODE_NAME}': d.hostname.replace(/[^a-z0-9\-]/ig, '_'),
 | 
			
		||||
        '{TIME}': d.lastseen.format('DDMMYYYYHmmss'),
 | 
			
		||||
        '{LOCALE}': _.locale()
 | 
			
		||||
      };
 | 
			
		||||
      return helper.showStat(V, o, subst);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return function (config, el, router, d) {
 | 
			
		||||
      var linkScale = d3Interpolate.interpolate('#F02311', '#04C714');
 | 
			
		||||
 | 
			
		||||
      function renderNeighbourRow(n) {
 | 
			
		||||
        var icons = [];
 | 
			
		||||
        icons.push(V.h('span', { props: { className: n.incoming ? 'ion-arrow-left-c' : 'ion-arrow-right-c' } }));
 | 
			
		||||
        if (helper.hasLocation(n.node)) {
 | 
			
		||||
          icons.push(V.h('span', { props: { className: 'ion-location' } }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var name = V.h('a', {
 | 
			
		||||
    return function (el, d, linkScale, nodeDict) {
 | 
			
		||||
      function nodeLink(node) {
 | 
			
		||||
        return V.h('a', {
 | 
			
		||||
          props: {
 | 
			
		||||
            className: 'online',
 | 
			
		||||
            href: router.generateLink({ node: n.node.nodeinfo.node_id })
 | 
			
		||||
            className: node.is_online ? 'online' : 'offline',
 | 
			
		||||
            href: router.generateLink({ node: node.node_id })
 | 
			
		||||
          }, on: {
 | 
			
		||||
            click: function (e) {
 | 
			
		||||
              router.fullUrl({ node: n.node.nodeinfo.node_id }, e);
 | 
			
		||||
              router.fullUrl({ node: node.node_id }, e);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, n.node.nodeinfo.hostname);
 | 
			
		||||
 | 
			
		||||
        var td1 = V.h('td', icons);
 | 
			
		||||
        var td2 = V.h('td', name);
 | 
			
		||||
        var td3 = V.h('td', (n.node.statistics.clients ? n.node.statistics.clients.toString() : '0'));
 | 
			
		||||
        var td4 = V.h('td', { style: { color: linkScale(1 / n.link.tq) } }, helper.showTq(n.link));
 | 
			
		||||
        var td5 = V.h('td', helper.showDistance(n.link));
 | 
			
		||||
 | 
			
		||||
        return V.h('tr', [td1, td2, td3, td4, td5]);
 | 
			
		||||
        }, node.hostname);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var h2 = document.createElement('h2');
 | 
			
		||||
      h2.textContent = d.nodeinfo.hostname;
 | 
			
		||||
      el.appendChild(h2);
 | 
			
		||||
 | 
			
		||||
      var attributes = document.createElement('table');
 | 
			
		||||
      attributes.classList.add('attributes');
 | 
			
		||||
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.status', showStatus(d));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.gateway', d.flags.gateway ? 'ja' : null);
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.coordinates', showGeoURI(d));
 | 
			
		||||
 | 
			
		||||
      if (config.nodeInfobox && config.nodeInfobox.contact) {
 | 
			
		||||
        helper.attributeEntry(attributes, 'node.contact', helper.dictGet(d.nodeinfo, ['owner', 'contact']));
 | 
			
		||||
      function nodeIdLink(nodeId) {
 | 
			
		||||
        if (nodeDict[nodeId]) {
 | 
			
		||||
          return nodeLink(nodeDict[nodeId]);
 | 
			
		||||
        }
 | 
			
		||||
        return nodeId;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.hardware', helper.dictGet(d.nodeinfo, ['hardware', 'model']));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.primaryMac', helper.dictGet(d.nodeinfo, ['network', 'mac']));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.id', helper.dictGet(d.nodeinfo, ['node_id']));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.firmware', showFirmware(d));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.site', showSite(d, config));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.uptime', showUptime(d));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.firstSeen', showFirstseen(d));
 | 
			
		||||
      if (config.nodeInfobox && config.nodeInfobox.hardwareUsage) {
 | 
			
		||||
        helper.attributeEntry(attributes, 'node.systemLoad', showLoad(d));
 | 
			
		||||
        helper.attributeEntry(attributes, 'node.ram', showRAM(d));
 | 
			
		||||
      }
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.ipAddresses', showIPs(d));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.selectedGateway', helper.dictGet(d.statistics, ['gateway']));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.update', showAutoupdate(d));
 | 
			
		||||
      helper.attributeEntry(attributes, 'node.clients', showClients(d));
 | 
			
		||||
      function showGateway(node) {
 | 
			
		||||
        var gatewayCols = [
 | 
			
		||||
          V.h('span', [
 | 
			
		||||
            nodeIdLink(node.gateway_nexthop),
 | 
			
		||||
            V.h('br'),
 | 
			
		||||
            _.t('node.nexthop')
 | 
			
		||||
          ]),
 | 
			
		||||
          V.h('span', { props: { className: 'ion-arrow-right-c' } }),
 | 
			
		||||
          V.h('span', [
 | 
			
		||||
            nodeIdLink(node.gateway),
 | 
			
		||||
            V.h('br'),
 | 
			
		||||
            'IPv4'
 | 
			
		||||
          ])
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
      el.appendChild(attributes);
 | 
			
		||||
        if (node.gateway6 !== undefined) {
 | 
			
		||||
          gatewayCols.push(V.h('span', [
 | 
			
		||||
            nodeIdLink(node.gateway6),
 | 
			
		||||
            V.h('br'),
 | 
			
		||||
            'IPv6'
 | 
			
		||||
          ]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (d.neighbours.length > 0) {
 | 
			
		||||
        var h3 = document.createElement('h3');
 | 
			
		||||
        h3.textContent = _.t('node.link', d.neighbours.length) + '(' + d.neighbours.length + ')';
 | 
			
		||||
        el.appendChild(h3);
 | 
			
		||||
 | 
			
		||||
        var headings = [{
 | 
			
		||||
          name: ''
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'node.nodes',
 | 
			
		||||
          sort: function (a, b) {
 | 
			
		||||
            return a.node.nodeinfo.hostname.localeCompare(b.node.nodeinfo.hostname);
 | 
			
		||||
          },
 | 
			
		||||
          reverse: false
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'node.clients',
 | 
			
		||||
          class: 'ion-people',
 | 
			
		||||
          sort: function (a, b) {
 | 
			
		||||
            return ('clients' in a.node.statistics ? a.node.statistics.clients : -1) -
 | 
			
		||||
              ('clients' in b.node.statistics ? b.node.statistics.clients : -1);
 | 
			
		||||
          },
 | 
			
		||||
          reverse: true
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'node.tq',
 | 
			
		||||
          class: 'ion-connection-bars',
 | 
			
		||||
          sort: function (a, b) {
 | 
			
		||||
            return a.link.tq - b.link.tq;
 | 
			
		||||
          },
 | 
			
		||||
          reverse: true
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'node.distance',
 | 
			
		||||
          class: 'ion-arrow-resize',
 | 
			
		||||
          sort: function (a, b) {
 | 
			
		||||
            return (a.link.distance === undefined ? -1 : a.link.distance) -
 | 
			
		||||
              (b.link.distance === undefined ? -1 : b.link.distance);
 | 
			
		||||
          },
 | 
			
		||||
          reverse: true
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
        var table = new SortTable(headings, 1, renderNeighbourRow);
 | 
			
		||||
        table.setData(d.neighbours);
 | 
			
		||||
        table.el.elm.classList.add('node-links');
 | 
			
		||||
        el.appendChild(table.el.elm);
 | 
			
		||||
        return V.h('td', { props: { className: 'gateway' } }, gatewayCols);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (config.nodeInfos) {
 | 
			
		||||
        config.nodeInfos.forEach(function (nodeInfo) {
 | 
			
		||||
          var h4 = document.createElement('h4');
 | 
			
		||||
          h4.textContent = nodeInfo.name;
 | 
			
		||||
          el.appendChild(h4);
 | 
			
		||||
          el.appendChild(showStatImg(nodeInfo, d));
 | 
			
		||||
      function renderNeighbourRow(n) {
 | 
			
		||||
        var icons = [V.h('span', { props: { className: 'icon ion-' + (n.link.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(n.link.type) } })];
 | 
			
		||||
        if (helper.hasLocation(n.node)) {
 | 
			
		||||
          icons.push(V.h('span', { props: { className: 'ion-location', title: _.t('location.location') } }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return V.h('tr', [
 | 
			
		||||
          V.h('td', icons),
 | 
			
		||||
          V.h('td', nodeLink(n.node)),
 | 
			
		||||
          V.h('td', n.node.clients),
 | 
			
		||||
          V.h('td', [V.h('a', {
 | 
			
		||||
            style: {
 | 
			
		||||
              color: linkScale((n.link.source_tq + n.link.target_tq) / 2)
 | 
			
		||||
            },
 | 
			
		||||
            props: {
 | 
			
		||||
              title: n.link.source.hostname + ' - ' + n.link.target.hostname,
 | 
			
		||||
              href: router.generateLink({ link: n.link.id })
 | 
			
		||||
            }, on: {
 | 
			
		||||
              click: function (e) {
 | 
			
		||||
                router.fullUrl({ link: n.link.id }, e);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }, helper.showTq(n.link.source_tq) + ' - ' + helper.showTq(n.link.target_tq))]),
 | 
			
		||||
          V.h('td', helper.showDistance(n.link))
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var self = this;
 | 
			
		||||
      var header = document.createElement('h2');
 | 
			
		||||
      var table = document.createElement('table');
 | 
			
		||||
      var images = document.createElement('div');
 | 
			
		||||
      var neighbours = document.createElement('h3');
 | 
			
		||||
      var headings = [{
 | 
			
		||||
        name: '',
 | 
			
		||||
        sort: function (a, b) {
 | 
			
		||||
          return a.link.type.localeCompare(b.link.type);
 | 
			
		||||
        }
 | 
			
		||||
      }, {
 | 
			
		||||
        name: 'node.nodes',
 | 
			
		||||
        sort: function (a, b) {
 | 
			
		||||
          return a.node.hostname.localeCompare(b.node.hostname);
 | 
			
		||||
        },
 | 
			
		||||
        reverse: false
 | 
			
		||||
      }, {
 | 
			
		||||
        name: 'node.clients',
 | 
			
		||||
        class: 'ion-people',
 | 
			
		||||
        sort: function (a, b) {
 | 
			
		||||
          return a.node.clients - b.node.clients;
 | 
			
		||||
        },
 | 
			
		||||
        reverse: true
 | 
			
		||||
      }, {
 | 
			
		||||
        name: 'node.tq',
 | 
			
		||||
        class: 'ion-connection-bars',
 | 
			
		||||
        sort: function (a, b) {
 | 
			
		||||
          return a.link.source_tq - b.link.source_tq;
 | 
			
		||||
        },
 | 
			
		||||
        reverse: true
 | 
			
		||||
      }, {
 | 
			
		||||
        name: 'node.distance',
 | 
			
		||||
        class: 'ion-arrow-resize',
 | 
			
		||||
        sort: function (a, b) {
 | 
			
		||||
          return (a.link.distance === undefined ? -1 : a.link.distance) -
 | 
			
		||||
            (b.link.distance === undefined ? -1 : b.link.distance);
 | 
			
		||||
        },
 | 
			
		||||
        reverse: true
 | 
			
		||||
      }];
 | 
			
		||||
      var tableNeighbour = new SortTable(headings, 1, renderNeighbourRow);
 | 
			
		||||
 | 
			
		||||
      el.appendChild(header);
 | 
			
		||||
      el.appendChild(table);
 | 
			
		||||
      el.appendChild(neighbours);
 | 
			
		||||
      el.appendChild(tableNeighbour.el);
 | 
			
		||||
      el.appendChild(images);
 | 
			
		||||
 | 
			
		||||
      self.render = function render() {
 | 
			
		||||
        V.patch(header, V.h('h2', d.hostname));
 | 
			
		||||
 | 
			
		||||
        var children = [];
 | 
			
		||||
 | 
			
		||||
        config.nodeAttr.forEach(function (row) {
 | 
			
		||||
          var field = d[row.value];
 | 
			
		||||
          if (typeof row.value === 'function') {
 | 
			
		||||
            field = row.value(d, nodeDict);
 | 
			
		||||
          } else if (nodef['show' + row.value] !== undefined) {
 | 
			
		||||
            field = nodef['show' + row.value](d);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (field) {
 | 
			
		||||
            if (typeof field !== 'object') {
 | 
			
		||||
              field = V.h('td', field);
 | 
			
		||||
            }
 | 
			
		||||
            children.push(V.h('tr', [
 | 
			
		||||
              row.name !== undefined ? V.h('th', _.t(row.name)) : null,
 | 
			
		||||
              field
 | 
			
		||||
            ]));
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        children.push(V.h('tr', [
 | 
			
		||||
          V.h('th', _.t('node.gateway')),
 | 
			
		||||
          showGateway(d)
 | 
			
		||||
        ]));
 | 
			
		||||
 | 
			
		||||
        var elNew = V.h('table', children);
 | 
			
		||||
        table = V.patch(table, elNew);
 | 
			
		||||
        table.elm.classList.add('attributes');
 | 
			
		||||
 | 
			
		||||
        V.patch(neighbours, V.h('h3', _.t('node.link', d.neighbours.length) + ' (' + d.neighbours.length + ')'));
 | 
			
		||||
        if (d.neighbours.length > 0) {
 | 
			
		||||
          tableNeighbour.setData(d.neighbours);
 | 
			
		||||
          tableNeighbour.el.elm.classList.add('node-links');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (config.nodeInfos) {
 | 
			
		||||
          var img = [];
 | 
			
		||||
          config.nodeInfos.forEach(function (nodeInfo) {
 | 
			
		||||
            img.push(V.h('h4', nodeInfo.name));
 | 
			
		||||
            img.push(showStatImg(nodeInfo, d));
 | 
			
		||||
          });
 | 
			
		||||
          images = V.patch(images, V.h('div', img));
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.setData = function setData(data) {
 | 
			
		||||
        if (data.nodeDict[d.node_id]) {
 | 
			
		||||
          d = data.nodeDict[d.node_id];
 | 
			
		||||
        }
 | 
			
		||||
        self.render();
 | 
			
		||||
      };
 | 
			
		||||
      return self;
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -1,46 +1,53 @@
 | 
			
		||||
define(['helper'], function (helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  return function (config, language) {
 | 
			
		||||
  return function (language) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    var stats = document.createTextNode('');
 | 
			
		||||
    var timestamp = document.createTextNode('');
 | 
			
		||||
 | 
			
		||||
    self.setData = function setData(d) {
 | 
			
		||||
      var totalNodes = helper.sum(d.nodes.all.map(helper.one));
 | 
			
		||||
      var totalOnlineNodes = helper.sum(d.nodes.all.filter(helper.online).map(helper.one));
 | 
			
		||||
      var totalClients = helper.sum(d.nodes.all.filter(helper.online).map(function (n) {
 | 
			
		||||
        return n.statistics.clients ? n.statistics.clients : 0;
 | 
			
		||||
      var totalNodes = Object.keys(d.nodeDict).length;
 | 
			
		||||
      var totalOnlineNodes = d.nodes.online.length;
 | 
			
		||||
      var totalClients = helper.sum(d.nodes.online.map(function (n) {
 | 
			
		||||
        return n.clients;
 | 
			
		||||
      }));
 | 
			
		||||
      var totalGateways = helper.sum(d.nodes.all.filter(helper.online).filter(function (n) {
 | 
			
		||||
        return n.flags.gateway;
 | 
			
		||||
      var totalGateways = helper.sum(d.nodes.online.filter(function (n) {
 | 
			
		||||
        return n.is_gateway;
 | 
			
		||||
      }).map(helper.one));
 | 
			
		||||
 | 
			
		||||
      stats.textContent = _.t('sidebar.nodes', { total: totalNodes, online: totalOnlineNodes }) + ' ' +
 | 
			
		||||
        _.t('sidebar.clients', { smart_count: totalClients }) + ' ' +
 | 
			
		||||
        _.t('sidebar.gateway', { smart_count: totalGateways });
 | 
			
		||||
 | 
			
		||||
      timestamp.textContent = _.t('sidebar.lastUpdate') + ': ' + d.timestamp.format('DD.MM.Y HH:mm');
 | 
			
		||||
      timestamp.textContent = _.t('sidebar.lastUpdate') + ' ' + d.timestamp.fromNow();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.render = function render(el) {
 | 
			
		||||
      var h2 = document.createElement('h2');
 | 
			
		||||
      h2.textContent = config.siteName;
 | 
			
		||||
      el.appendChild(h2);
 | 
			
		||||
      var h1 = document.createElement('h1');
 | 
			
		||||
      h1.textContent = config.siteName;
 | 
			
		||||
      el.appendChild(h1);
 | 
			
		||||
 | 
			
		||||
      language.languageSelect(el);
 | 
			
		||||
 | 
			
		||||
      var p = document.createElement('p');
 | 
			
		||||
      p.classList.add('legend');
 | 
			
		||||
      p.innerHTML = '<span class="legend-new"><span class="symbol"></span> ' + _.t('sidebar.nodeNew') + '</span>' +
 | 
			
		||||
        '<span class="legend-online"><span class="symbol"></span> ' + _.t('sidebar.nodeOnline') + '</span>' +
 | 
			
		||||
        '<span class="legend-offline"><span class="symbol"></span> ' + _.t('sidebar.nodeOffline') + '</span>';
 | 
			
		||||
      el.appendChild(p);
 | 
			
		||||
 | 
			
		||||
      p.appendChild(document.createElement('br'));
 | 
			
		||||
      p.appendChild(stats);
 | 
			
		||||
      p.appendChild(document.createElement('br'));
 | 
			
		||||
      p.appendChild(timestamp);
 | 
			
		||||
 | 
			
		||||
      if (config.linkList) {
 | 
			
		||||
        p.appendChild(document.createElement('br'));
 | 
			
		||||
        config.linkList.forEach(function (link) {
 | 
			
		||||
          var a = document.createElement('a');
 | 
			
		||||
          a.innerText = link.title;
 | 
			
		||||
          a.href = link.href;
 | 
			
		||||
          p.appendChild(a);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      el.appendChild(p);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return self;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,17 @@
 | 
			
		||||
define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  V = V.default;
 | 
			
		||||
 | 
			
		||||
  function linkName(d) {
 | 
			
		||||
    return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname;
 | 
			
		||||
    return (d.source ? d.source.hostname : d.source.id) + ' – ' + d.target.hostname;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var headings = [{
 | 
			
		||||
    name: '',
 | 
			
		||||
    sort: function (a, b) {
 | 
			
		||||
      return a.type.localeCompare(b.type);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    name: 'node.nodes',
 | 
			
		||||
    sort: function (a, b) {
 | 
			
		||||
      return linkName(a).localeCompare(linkName(b));
 | 
			
		||||
@ -15,7 +21,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
    name: 'node.tq',
 | 
			
		||||
    class: 'ion-connection-bars',
 | 
			
		||||
    sort: function (a, b) {
 | 
			
		||||
      return a.tq - b.tq;
 | 
			
		||||
      return (a.source_tq + a.target_tq) / 2 - (b.source_tq + b.target_tq) / 2;
 | 
			
		||||
    },
 | 
			
		||||
    reverse: true
 | 
			
		||||
  }, {
 | 
			
		||||
@ -28,9 +34,8 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
    reverse: true
 | 
			
		||||
  }];
 | 
			
		||||
 | 
			
		||||
  return function (linkScale, router) {
 | 
			
		||||
    var table = new SortTable(headings, 2, renderRow);
 | 
			
		||||
    V = V.default;
 | 
			
		||||
  return function (linkScale) {
 | 
			
		||||
    var table = new SortTable(headings, 3, renderRow);
 | 
			
		||||
 | 
			
		||||
    function renderRow(d) {
 | 
			
		||||
      var td1Content = [V.h('a', {
 | 
			
		||||
@ -43,11 +48,12 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
        }
 | 
			
		||||
      }, linkName(d))];
 | 
			
		||||
 | 
			
		||||
      var td1 = V.h('td', td1Content);
 | 
			
		||||
      var td2 = V.h('td', {  style: { color: linkScale(1 / d.tq) } }, helper.showTq(d));
 | 
			
		||||
      var td3 = V.h('td', helper.showDistance(d));
 | 
			
		||||
 | 
			
		||||
      return V.h('tr', [td1, td2, td3]);
 | 
			
		||||
      return V.h('tr', [
 | 
			
		||||
        V.h('td', V.h('span', { props: { className: 'icon ion-' + (d.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(d.type) } })),
 | 
			
		||||
        V.h('td', td1Content),
 | 
			
		||||
        V.h('td', { style: { color: linkScale((d.source_tq + d.target_tq) / 2) } }, helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq)),
 | 
			
		||||
        V.h('td', helper.showDistance(d))
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.render = function render(d) {
 | 
			
		||||
@ -59,7 +65,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setData = function setData(d) {
 | 
			
		||||
      table.setData(d.graph.links);
 | 
			
		||||
      table.setData(d.links);
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										157
									
								
								lib/main.js
									
									
									
									
									
								
							
							
						
						@ -2,105 +2,53 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
 | 
			
		||||
  function (moment, Router, L, GUI, helper, Language) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
 | 
			
		||||
    return function (config) {
 | 
			
		||||
    return function () {
 | 
			
		||||
      function handleData(data) {
 | 
			
		||||
        var dataNodes = {};
 | 
			
		||||
        dataNodes.nodes = [];
 | 
			
		||||
        var dataGraph = {};
 | 
			
		||||
        dataGraph.batadv = {};
 | 
			
		||||
        dataGraph.batadv.nodes = [];
 | 
			
		||||
        dataGraph.batadv.links = [];
 | 
			
		||||
 | 
			
		||||
        function rearrangeLinks(d) {
 | 
			
		||||
          d.source += dataGraph.batadv.nodes.length;
 | 
			
		||||
          d.target += dataGraph.batadv.nodes.length;
 | 
			
		||||
        }
 | 
			
		||||
        var timestamp;
 | 
			
		||||
        var nodes = [];
 | 
			
		||||
        var links = [];
 | 
			
		||||
        var nodeDict = {};
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < data.length; ++i) {
 | 
			
		||||
          var vererr;
 | 
			
		||||
          if (i % 2) {
 | 
			
		||||
            if (data[i].version !== 1) {
 | 
			
		||||
              vererr = 'Unsupported graph version: ' + data[i].version;
 | 
			
		||||
              console.error(vererr); // silent fail
 | 
			
		||||
            } else {
 | 
			
		||||
              data[i].batadv.links.forEach(rearrangeLinks);
 | 
			
		||||
              dataGraph.batadv.nodes = dataGraph.batadv.nodes.concat(data[i].batadv.nodes);
 | 
			
		||||
              dataGraph.batadv.links = dataGraph.batadv.links.concat(data[i].batadv.links);
 | 
			
		||||
              dataGraph.timestamp = data[i].timestamp;
 | 
			
		||||
            }
 | 
			
		||||
          } else if (data[i].version !== 2) {
 | 
			
		||||
            vererr = 'Unsupported nodes version: ' + data[i].version;
 | 
			
		||||
            console.error(vererr); // silent fail
 | 
			
		||||
          } else {
 | 
			
		||||
            dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes);
 | 
			
		||||
            dataNodes.timestamp = data[i].timestamp;
 | 
			
		||||
          }
 | 
			
		||||
          nodes = nodes.concat(data[i].nodes);
 | 
			
		||||
          timestamp = data[i].timestamp;
 | 
			
		||||
          links = links.concat(data[i].links);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var nodes = dataNodes.nodes.filter(function (d) {
 | 
			
		||||
          return 'firstseen' in d && 'lastseen' in d;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        nodes.forEach(function (node) {
 | 
			
		||||
          node.firstseen = moment.utc(node.firstseen).local();
 | 
			
		||||
          node.lastseen = moment.utc(node.lastseen).local();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var now = moment();
 | 
			
		||||
        var age = moment(now).subtract(config.maxAge, 'days');
 | 
			
		||||
        var age = moment().subtract(config.maxAge, 'days');
 | 
			
		||||
 | 
			
		||||
        var newnodes = helper.limit('firstseen', age, helper.sortByKey('firstseen', nodes).filter(helper.online));
 | 
			
		||||
        var lostnodes = helper.limit('lastseen', age, helper.sortByKey('lastseen', nodes).filter(helper.offline));
 | 
			
		||||
 | 
			
		||||
        var graphnodes = {};
 | 
			
		||||
 | 
			
		||||
        dataNodes.nodes.forEach(function (d) {
 | 
			
		||||
          graphnodes[d.nodeinfo.node_id] = d;
 | 
			
		||||
        var online = nodes.filter(function (d) {
 | 
			
		||||
          return d.is_online;
 | 
			
		||||
        });
 | 
			
		||||
        var offline = nodes.filter(function (d) {
 | 
			
		||||
          return !d.is_online;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var graph = dataGraph.batadv;
 | 
			
		||||
 | 
			
		||||
        graph.nodes.forEach(function (d) {
 | 
			
		||||
          if (d.node_id in graphnodes) {
 | 
			
		||||
            d.node = graphnodes[d.node_id];
 | 
			
		||||
            if (d.unseen) {
 | 
			
		||||
              d.node.flags.online = true;
 | 
			
		||||
              d.node.flags.unseen = true;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        graph.links.forEach(function (d) {
 | 
			
		||||
          d.source = graph.nodes[d.source];
 | 
			
		||||
 | 
			
		||||
          if (graph.nodes[d.target].node) {
 | 
			
		||||
            d.target = graph.nodes[d.target];
 | 
			
		||||
          } else {
 | 
			
		||||
            d.target = undefined;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var links = graph.links.filter(function (d) {
 | 
			
		||||
          return d.target !== undefined;
 | 
			
		||||
        });
 | 
			
		||||
        var newnodes = helper.limit('firstseen', age, helper.sortByKey('firstseen', online));
 | 
			
		||||
        var lostnodes = helper.limit('lastseen', age, helper.sortByKey('lastseen', offline));
 | 
			
		||||
 | 
			
		||||
        nodes.forEach(function (d) {
 | 
			
		||||
          d.neighbours = [];
 | 
			
		||||
          nodeDict[d.node_id] = d;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        links.forEach(function (d) {
 | 
			
		||||
          var ids;
 | 
			
		||||
          d.source = nodeDict[d.source];
 | 
			
		||||
          d.target = nodeDict[d.target];
 | 
			
		||||
 | 
			
		||||
          ids = [d.source.node.nodeinfo.node_id, d.target.node.nodeinfo.node_id];
 | 
			
		||||
          d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false });
 | 
			
		||||
          d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true });
 | 
			
		||||
 | 
			
		||||
          d.id = ids.join('-');
 | 
			
		||||
          d.id = [d.source.node_id, d.target.node_id].join('-');
 | 
			
		||||
          d.source.neighbours.push({ node: d.target, link: d });
 | 
			
		||||
          d.target.neighbours.push({ node: d.source, link: d });
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            d.latlngs = [];
 | 
			
		||||
            d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude));
 | 
			
		||||
            d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude));
 | 
			
		||||
            d.latlngs.push(L.latLng(d.source.location.latitude, d.source.location.longitude));
 | 
			
		||||
            d.latlngs.push(L.latLng(d.target.location.latitude, d.target.location.longitude));
 | 
			
		||||
 | 
			
		||||
            d.distance = d.latlngs[0].distanceTo(d.latlngs[1]);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
@ -108,50 +56,53 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        links.sort(function (a, b) {
 | 
			
		||||
          return b.tq - a.tq;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          now: now,
 | 
			
		||||
          timestamp: moment.utc(dataNodes.timestamp).local(),
 | 
			
		||||
          now: moment(),
 | 
			
		||||
          timestamp: moment.utc(timestamp).local(),
 | 
			
		||||
          nodes: {
 | 
			
		||||
            all: nodes,
 | 
			
		||||
            online: online,
 | 
			
		||||
            offline: offline,
 | 
			
		||||
            new: newnodes,
 | 
			
		||||
            lost: lostnodes
 | 
			
		||||
          },
 | 
			
		||||
          graph: {
 | 
			
		||||
            links: links,
 | 
			
		||||
            nodes: graph.nodes
 | 
			
		||||
          }
 | 
			
		||||
          links: links,
 | 
			
		||||
          nodeDict: nodeDict
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var language = new Language(config);
 | 
			
		||||
      var router = new Router(language);
 | 
			
		||||
      var language = new Language();
 | 
			
		||||
      window.router = new Router(language);
 | 
			
		||||
 | 
			
		||||
      var urls = [];
 | 
			
		||||
      config.dataPath.forEach(function (d, i) {
 | 
			
		||||
        config.dataPath[i] += 'meshviewer.json';
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (typeof config.dataPath === 'string' || config.dataPath instanceof String) {
 | 
			
		||||
        config.dataPath = [config.dataPath];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var i in config.dataPath) {
 | 
			
		||||
        if (config.dataPath.hasOwnProperty(i)) {
 | 
			
		||||
          urls.push(config.dataPath[i] + 'nodes.json');
 | 
			
		||||
          urls.push(config.dataPath[i] + 'graph.json');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      language.init(router);
 | 
			
		||||
 | 
			
		||||
      function update() {
 | 
			
		||||
        language.init(router);
 | 
			
		||||
        return Promise.all(urls.map(helper.getJSON))
 | 
			
		||||
        return Promise.all(config.dataPath.map(helper.getJSON))
 | 
			
		||||
          .then(handleData);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      update()
 | 
			
		||||
        .then(function (d) {
 | 
			
		||||
          var gui = new GUI(config, router, language);
 | 
			
		||||
          return new Promise(function (resolve, reject) {
 | 
			
		||||
            var count = 0;
 | 
			
		||||
            (function waitForLanguage() {
 | 
			
		||||
              if (Object.keys(_.phrases).length > 0) {
 | 
			
		||||
                resolve(d);
 | 
			
		||||
              } else if (count > 500) {
 | 
			
		||||
                reject(new Error('translation not loaded after 10 seconds'));
 | 
			
		||||
              } else {
 | 
			
		||||
                setTimeout(waitForLanguage.bind(this), 20);
 | 
			
		||||
              }
 | 
			
		||||
              count++;
 | 
			
		||||
            })();
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
        .then(function (d) {
 | 
			
		||||
          var gui = new GUI(language);
 | 
			
		||||
          gui.setData(d);
 | 
			
		||||
          router.setData(d);
 | 
			
		||||
          router.resolve();
 | 
			
		||||
@ -165,7 +116,7 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function (e) {
 | 
			
		||||
          document.querySelector('.loader').innerHTML += e.message
 | 
			
		||||
            + '<br /><br /><button onclick="location.reload(true)" class="btn text">Try to reload</button><br /> or report to your community';
 | 
			
		||||
            + '<br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br /> or report to your community';
 | 
			
		||||
          console.warn(e);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								lib/map.js
									
									
									
									
									
								
							
							
						
						@ -1,4 +1,4 @@
 | 
			
		||||
define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet', 'map/activearea'],
 | 
			
		||||
  function (ClientLayer, LabelLayer, Button, L) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
      minZoom: 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return function (config, linkScale, sidebar, router, buttons) {
 | 
			
		||||
    return function (linkScale, sidebar, buttons) {
 | 
			
		||||
      var self = this;
 | 
			
		||||
      var savedView;
 | 
			
		||||
 | 
			
		||||
@ -27,10 +27,26 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
        document.querySelector('.leaflet-control-layers').classList.add('leaflet-control-layers-expanded');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function mapActiveArea() {
 | 
			
		||||
        map.setActiveArea({
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          left: sidebar.getWidth() + 'px',
 | 
			
		||||
          right: 0,
 | 
			
		||||
          top: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function setActiveArea() {
 | 
			
		||||
        setTimeout(mapActiveArea, 300);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var el = document.createElement('div');
 | 
			
		||||
      el.classList.add('map');
 | 
			
		||||
 | 
			
		||||
      map = L.map(el, options);
 | 
			
		||||
      mapActiveArea();
 | 
			
		||||
 | 
			
		||||
      var now = new Date();
 | 
			
		||||
      config.mapLayers.forEach(function (item, i) {
 | 
			
		||||
        if ((typeof item.config.start === 'number' && item.config.start <= now.getHours()) || (typeof item.config.end === 'number' && item.config.end > now.getHours())) {
 | 
			
		||||
@ -47,7 +63,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
      var layers = config.mapLayers.map(function (d) {
 | 
			
		||||
        return {
 | 
			
		||||
          'name': d.name,
 | 
			
		||||
          'layer': 'url' in d ? L.tileLayer(d.url.replace('{retina}', L.Browser.retina ? '@2x' : ''), d.config) : console.warn('Missing map url')
 | 
			
		||||
          'layer': L.tileLayer(d.url.replace('{retina}', L.Browser.retina ? '@2x' : ''), d.config)
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -57,13 +73,23 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
        baseLayers[d.name] = d.layer;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      var button = new Button(config, map, router, buttons);
 | 
			
		||||
      var button = new Button(map, buttons);
 | 
			
		||||
 | 
			
		||||
      map.on('locationfound', button.locationFound);
 | 
			
		||||
      map.on('locationerror', button.locationError);
 | 
			
		||||
      map.on('dragend', saveView);
 | 
			
		||||
      map.on('contextmenu', contextMenuOpenLayerMenu);
 | 
			
		||||
 | 
			
		||||
      if (config.geo) {
 | 
			
		||||
        [].forEach.call(config.geo, function (geo) {
 | 
			
		||||
          geo.json().then(function (result) {
 | 
			
		||||
            if (result) {
 | 
			
		||||
              L.geoJSON(result, geo.option).addTo(map);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      button.init();
 | 
			
		||||
 | 
			
		||||
      layerControl = L.control.layers(baseLayers, [], { position: 'bottomright' });
 | 
			
		||||
@ -79,6 +105,8 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
      labelLayer.addTo(map);
 | 
			
		||||
      labelLayer.setZIndex(6);
 | 
			
		||||
 | 
			
		||||
      sidebar.button.addEventListener('visibility', setActiveArea);
 | 
			
		||||
 | 
			
		||||
      map.on('zoom', function () {
 | 
			
		||||
        clientLayer.redraw();
 | 
			
		||||
        labelLayer.redraw();
 | 
			
		||||
@ -105,6 +133,14 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      map.on('load', function () {
 | 
			
		||||
        var inputs = document.querySelectorAll('.leaflet-control-layers-selector');
 | 
			
		||||
        [].forEach.call(inputs, function (input) {
 | 
			
		||||
          input.setAttribute('role', 'radiogroup');
 | 
			
		||||
          input.setAttribute('aria-label', input.nextSibling.innerHTML.trim());
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      var nodeDict = {};
 | 
			
		||||
      var linkDict = {};
 | 
			
		||||
      var highlight;
 | 
			
		||||
@ -120,7 +156,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function setView(bounds, zoom) {
 | 
			
		||||
        map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: (zoom ? zoom : config.nodeZoom) });
 | 
			
		||||
        map.fitBounds(bounds, { maxZoom: (zoom ? zoom : config.nodeZoom) });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function goto(m) {
 | 
			
		||||
@ -142,12 +178,12 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
        var m;
 | 
			
		||||
 | 
			
		||||
        if (highlight !== undefined) {
 | 
			
		||||
          if (highlight.type === 'node' && nodeDict[highlight.o.nodeinfo.node_id]) {
 | 
			
		||||
            m = nodeDict[highlight.o.nodeinfo.node_id];
 | 
			
		||||
            m.setStyle({ color: 'orange', weight: 20, fillOpacity: 1, opacity: 0.7, className: 'stroke-first' });
 | 
			
		||||
          if (highlight.type === 'node' && nodeDict[highlight.o.node_id]) {
 | 
			
		||||
            m = nodeDict[highlight.o.node_id];
 | 
			
		||||
            m.setStyle(config.map.highlightNode);
 | 
			
		||||
          } else if (highlight.type === 'link' && linkDict[highlight.o.id]) {
 | 
			
		||||
            m = linkDict[highlight.o.id];
 | 
			
		||||
            m.setStyle({ weight: 4, opacity: 1, dashArray: '5, 10' });
 | 
			
		||||
            m.setStyle(config.map.highlightLink);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -167,7 +203,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
        linkDict = {};
 | 
			
		||||
 | 
			
		||||
        clientLayer.setData(data);
 | 
			
		||||
        labelLayer.setData(data, map, nodeDict, linkDict, linkScale, router, config);
 | 
			
		||||
        labelLayer.setData(data, map, nodeDict, linkDict, linkScale);
 | 
			
		||||
 | 
			
		||||
        updateView(true);
 | 
			
		||||
      };
 | 
			
		||||
@ -186,7 +222,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
 | 
			
		||||
      self.gotoLink = function gotoLink(d) {
 | 
			
		||||
        button.disableTracking();
 | 
			
		||||
        highlight = { type: 'link', o: d };
 | 
			
		||||
        highlight = { type: 'link', o: d[0] };
 | 
			
		||||
        updateView();
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -197,6 +233,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
 | 
			
		||||
 | 
			
		||||
      self.destroy = function destroy() {
 | 
			
		||||
        button.clearButtons();
 | 
			
		||||
        sidebar.button.removeEventListener('visibility', setActiveArea);
 | 
			
		||||
        map.remove();
 | 
			
		||||
 | 
			
		||||
        if (el.parentNode) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										291
									
								
								lib/map/activearea.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,291 @@
 | 
			
		||||
define(function () {
 | 
			
		||||
  /**
 | 
			
		||||
   * https://github.com/Mappy/Leaflet-active-area
 | 
			
		||||
   * Apache 2.0 license https://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  var previousMethods = {
 | 
			
		||||
    getCenter: L.Map.prototype.getCenter,
 | 
			
		||||
    setView: L.Map.prototype.setView,
 | 
			
		||||
    setZoomAround: L.Map.prototype.setZoomAround,
 | 
			
		||||
    getBoundsZoom: L.Map.prototype.getBoundsZoom,
 | 
			
		||||
    RendererUpdate: L.Renderer.prototype._update
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  L.Map.include({
 | 
			
		||||
    getBounds: function () {
 | 
			
		||||
      if (this._viewport) {
 | 
			
		||||
        return this.getViewportLatLngBounds();
 | 
			
		||||
      }
 | 
			
		||||
      var bounds = this.getPixelBounds();
 | 
			
		||||
      var sw = this.unproject(bounds.getBottomLeft());
 | 
			
		||||
      var ne = this.unproject(bounds.getTopRight());
 | 
			
		||||
 | 
			
		||||
      return new L.LatLngBounds(sw, ne);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getViewport: function () {
 | 
			
		||||
      return this._viewport;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getViewportBounds: function () {
 | 
			
		||||
      var vp = this._viewport;
 | 
			
		||||
      var topleft = L.point(vp.offsetLeft, vp.offsetTop);
 | 
			
		||||
      var vpsize = L.point(vp.clientWidth, vp.clientHeight);
 | 
			
		||||
 | 
			
		||||
      if (vpsize.x === 0 || vpsize.y === 0) {
 | 
			
		||||
        // Our own viewport has no good size - so we fallback to the container size:
 | 
			
		||||
        vp = this.getContainer();
 | 
			
		||||
        if (vp) {
 | 
			
		||||
          topleft = L.point(0, 0);
 | 
			
		||||
          vpsize = L.point(vp.clientWidth, vp.clientHeight);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return L.bounds(topleft, topleft.add(vpsize));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getViewportLatLngBounds: function () {
 | 
			
		||||
      var bounds = this.getViewportBounds();
 | 
			
		||||
      return L.latLngBounds(this.containerPointToLatLng(bounds.min), this.containerPointToLatLng(bounds.max));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getOffset: function () {
 | 
			
		||||
      var mCenter = this.getSize().divideBy(2);
 | 
			
		||||
      var vCenter = this.getViewportBounds().getCenter();
 | 
			
		||||
 | 
			
		||||
      return mCenter.subtract(vCenter);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getCenter: function (withoutViewport) {
 | 
			
		||||
      var center = previousMethods.getCenter.call(this);
 | 
			
		||||
 | 
			
		||||
      if (this.getViewport() && !withoutViewport) {
 | 
			
		||||
        var zoom = this.getZoom();
 | 
			
		||||
        var point = this.project(center, zoom);
 | 
			
		||||
        point = point.subtract(this.getOffset());
 | 
			
		||||
 | 
			
		||||
        center = this.unproject(point, zoom);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return center;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setView: function (center, zoom, options) {
 | 
			
		||||
      center = L.latLng(center);
 | 
			
		||||
      zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
 | 
			
		||||
 | 
			
		||||
      if (this.getViewport()) {
 | 
			
		||||
        var point = this.project(center, this._limitZoom(zoom));
 | 
			
		||||
        point = point.add(this.getOffset());
 | 
			
		||||
        center = this.unproject(point, this._limitZoom(zoom));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return previousMethods.setView.call(this, center, zoom, options);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setZoomAround: function (latlng, zoom, options) {
 | 
			
		||||
      var vp = this.getViewport();
 | 
			
		||||
 | 
			
		||||
      if (vp) {
 | 
			
		||||
        var scale = this.getZoomScale(zoom);
 | 
			
		||||
        var viewHalf = this.getViewportBounds().getCenter();
 | 
			
		||||
        var containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng);
 | 
			
		||||
 | 
			
		||||
        var centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale);
 | 
			
		||||
        var newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
 | 
			
		||||
 | 
			
		||||
        return this.setView(newCenter, zoom, { zoom: options });
 | 
			
		||||
      }
 | 
			
		||||
      return previousMethods.setZoomAround.call(this, latlng, zoom, options);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
 | 
			
		||||
      bounds = L.latLngBounds(bounds);
 | 
			
		||||
      padding = L.point(padding || [0, 0]);
 | 
			
		||||
 | 
			
		||||
      var zoom = this.getZoom() || 0;
 | 
			
		||||
      var min = this.getMinZoom();
 | 
			
		||||
      var max = this.getMaxZoom();
 | 
			
		||||
      var nw = bounds.getNorthWest();
 | 
			
		||||
      var se = bounds.getSouthEast();
 | 
			
		||||
      var vp = this.getViewport();
 | 
			
		||||
      var size = (vp ? L.point(vp.clientWidth, vp.clientHeight) : this.getSize()).subtract(padding);
 | 
			
		||||
      var boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom));
 | 
			
		||||
      var snap = L.Browser.any3d ? this.options.zoomSnap : 1;
 | 
			
		||||
 | 
			
		||||
      var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
 | 
			
		||||
 | 
			
		||||
      zoom = this.getScaleZoom(scale, zoom);
 | 
			
		||||
 | 
			
		||||
      if (snap) {
 | 
			
		||||
        zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
 | 
			
		||||
        zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return Math.max(min, Math.min(max, zoom));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  L.Map.include({
 | 
			
		||||
    setActiveArea: function (css, keepCenter, animate) {
 | 
			
		||||
      var center;
 | 
			
		||||
      if (keepCenter && this._zoom) {
 | 
			
		||||
        // save center if map is already initialized
 | 
			
		||||
        // and keepCenter is passed
 | 
			
		||||
        center = this.getCenter();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this._viewport) {
 | 
			
		||||
        // Make viewport if not already made
 | 
			
		||||
        var container = this.getContainer();
 | 
			
		||||
        this._viewport = L.DomUtil.create('div', '');
 | 
			
		||||
        container.insertBefore(this._viewport, container.firstChild);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (typeof css === 'string') {
 | 
			
		||||
        this._viewport.className = css;
 | 
			
		||||
      } else {
 | 
			
		||||
        L.extend(this._viewport.style, css);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (center) {
 | 
			
		||||
        this.setView(center, this.getZoom(), { animate: !!animate });
 | 
			
		||||
      }
 | 
			
		||||
      return this;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  L.Renderer.include({
 | 
			
		||||
    _onZoom: function () {
 | 
			
		||||
      this._updateTransform(this._map.getCenter(true), this._map.getZoom());
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _update: function () {
 | 
			
		||||
      previousMethods.RendererUpdate.call(this);
 | 
			
		||||
      this._center = this._map.getCenter(true);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  L.GridLayer.include({
 | 
			
		||||
    _updateLevels: function () {
 | 
			
		||||
      var zoom = this._tileZoom;
 | 
			
		||||
      var maxZoom = this.options.maxZoom;
 | 
			
		||||
 | 
			
		||||
      if (zoom === undefined) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var z in this._levels) {
 | 
			
		||||
        if (this._levels[z].el.children.length || z === zoom) {
 | 
			
		||||
          this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
 | 
			
		||||
        } else {
 | 
			
		||||
          L.DomUtil.remove(this._levels[z].el);
 | 
			
		||||
          this._removeTilesAtZoom(z);
 | 
			
		||||
          delete this._levels[z];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var level = this._levels[zoom];
 | 
			
		||||
      var map = this._map;
 | 
			
		||||
 | 
			
		||||
      if (!level) {
 | 
			
		||||
        level = this._levels[zoom] = {};
 | 
			
		||||
 | 
			
		||||
        level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
 | 
			
		||||
        level.el.style.zIndex = maxZoom;
 | 
			
		||||
 | 
			
		||||
        level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
 | 
			
		||||
        level.zoom = zoom;
 | 
			
		||||
 | 
			
		||||
        this._setZoomTransform(level, map.getCenter(true), map.getZoom());
 | 
			
		||||
 | 
			
		||||
        // force the browser to consider the newly added element for transition
 | 
			
		||||
        L.Util.falseFn(level.el.offsetWidth);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this._level = level;
 | 
			
		||||
 | 
			
		||||
      return level;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _resetView: function (e) {
 | 
			
		||||
      var animating = e && (e.pinch || e.flyTo);
 | 
			
		||||
      this._setView(this._map.getCenter(true), this._map.getZoom(), animating, animating);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _update: function (center) {
 | 
			
		||||
      var map = this._map;
 | 
			
		||||
      if (!map) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      var zoom = map.getZoom();
 | 
			
		||||
 | 
			
		||||
      if (center === undefined) {
 | 
			
		||||
        center = map.getCenter(this);
 | 
			
		||||
      }
 | 
			
		||||
      if (this._tileZoom === undefined) {
 | 
			
		||||
        return;
 | 
			
		||||
      }    // if out of minzoom/maxzoom
 | 
			
		||||
 | 
			
		||||
      var pixelBounds = this._getTiledPixelBounds(center);
 | 
			
		||||
      var tileRange = this._pxBoundsToTileRange(pixelBounds);
 | 
			
		||||
      var tileCenter = tileRange.getCenter();
 | 
			
		||||
      var queue = [];
 | 
			
		||||
 | 
			
		||||
      for (var key in this._tiles) {
 | 
			
		||||
        this._tiles[key].current = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // _update just loads more tiles. If the tile zoom level differs too much
 | 
			
		||||
      // from the map's, let _setView reset levels and prune old tiles.
 | 
			
		||||
      if (Math.abs(zoom - this._tileZoom) > 1) {
 | 
			
		||||
        this._setView(center, zoom);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // create a queue of coordinates to load tiles from
 | 
			
		||||
      for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
 | 
			
		||||
        for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
 | 
			
		||||
          var coords = new L.Point(i, j);
 | 
			
		||||
          coords.z = this._tileZoom;
 | 
			
		||||
 | 
			
		||||
          if (!this._isValidTile(coords)) {
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          var tile = this._tiles[this._tileCoordsToKey(coords)];
 | 
			
		||||
          if (tile) {
 | 
			
		||||
            tile.current = true;
 | 
			
		||||
          } else {
 | 
			
		||||
            queue.push(coords);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // sort tile queue to load tiles in order of their distance to center
 | 
			
		||||
      queue.sort(function (a, b) {
 | 
			
		||||
        return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (queue.length !== 0) {
 | 
			
		||||
        // if its the first batch of tiles to load
 | 
			
		||||
        if (!this._loading) {
 | 
			
		||||
          this._loading = true;
 | 
			
		||||
          // @event loading: Event
 | 
			
		||||
          // Fired when the grid layer starts loading tiles
 | 
			
		||||
          this.fire('loading');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // create DOM fragment to append tiles in one batch
 | 
			
		||||
        var fragment = document.createDocumentFragment();
 | 
			
		||||
 | 
			
		||||
        for (i = 0; i < queue.length; i++) {
 | 
			
		||||
          this._addTile(queue[i], fragment);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._level.el.appendChild(fragment);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationmarker'],
 | 
			
		||||
  function (ClientLayer, LabelLayer, L, moment, LocationMarker) {
 | 
			
		||||
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'map/locationmarker'],
 | 
			
		||||
  function (ClientLayer, LabelLayer, L, LocationMarker) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
    var self = {};
 | 
			
		||||
 | 
			
		||||
@ -28,8 +28,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
 | 
			
		||||
 | 
			
		||||
    var LocateButton = ButtonBase.extend({
 | 
			
		||||
      onAdd: function () {
 | 
			
		||||
        var button = L.DomUtil.create('button', 'ion-locate shadow');
 | 
			
		||||
        button.setAttribute('data-tooltip', _.t('button.tracking'));
 | 
			
		||||
        var button = L.DomUtil.create('button', 'ion-locate');
 | 
			
		||||
        button.setAttribute('aria-label', _.t('button.tracking'));
 | 
			
		||||
        L.DomEvent.disableClickPropagation(button);
 | 
			
		||||
        L.DomEvent.addListener(button, 'click', this.onClick, this);
 | 
			
		||||
 | 
			
		||||
@ -45,8 +45,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
 | 
			
		||||
 | 
			
		||||
    var CoordsPickerButton = ButtonBase.extend({
 | 
			
		||||
      onAdd: function () {
 | 
			
		||||
        var button = L.DomUtil.create('button', 'ion-pin shadow');
 | 
			
		||||
        button.setAttribute('data-tooltip', _.t('button.location'));
 | 
			
		||||
        var button = L.DomUtil.create('button', 'ion-pin');
 | 
			
		||||
        button.setAttribute('aria-label', _.t('button.location'));
 | 
			
		||||
 | 
			
		||||
        // Click propagation isn't disabled as this causes problems with the
 | 
			
		||||
        // location picking mode; instead propagation is stopped in onClick().
 | 
			
		||||
@ -63,7 +63,7 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return function (config, map, router, buttons) {
 | 
			
		||||
    return function (map, buttons) {
 | 
			
		||||
      var userLocation;
 | 
			
		||||
 | 
			
		||||
      var locateUserButton = new LocateButton(function (d) {
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,23 @@
 | 
			
		||||
define(['leaflet', 'rbush', 'helper'],
 | 
			
		||||
  function (L, rbush, helper) {
 | 
			
		||||
  function (L, RBush, helper) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
 | 
			
		||||
    return L.GridLayer.extend({
 | 
			
		||||
      mapRTree: function mapRTree(d) {
 | 
			
		||||
        return {
 | 
			
		||||
          minX: d.nodeinfo.location.latitude, minY: d.nodeinfo.location.longitude,
 | 
			
		||||
          maxX: d.nodeinfo.location.latitude, maxY: d.nodeinfo.location.longitude,
 | 
			
		||||
          minX: d.location.latitude, minY: d.location.longitude,
 | 
			
		||||
          maxX: d.location.latitude, maxY: d.location.longitude,
 | 
			
		||||
          node: d
 | 
			
		||||
        };
 | 
			
		||||
      },
 | 
			
		||||
      setData: function (data) {
 | 
			
		||||
        var rtreeOnlineAll = rbush(9);
 | 
			
		||||
        var rtreeOnlineAll = new RBush(9);
 | 
			
		||||
 | 
			
		||||
        this.data = rtreeOnlineAll.load(data.nodes.all.filter(helper.online).filter(helper.hasLocation).map(this.mapRTree));
 | 
			
		||||
        this.data = rtreeOnlineAll.load(data.nodes.online.filter(helper.hasLocation).map(this.mapRTree));
 | 
			
		||||
 | 
			
		||||
        // pre-calculate start angles
 | 
			
		||||
        this.data.all().forEach(function (n) {
 | 
			
		||||
          n.startAngle = (parseInt(n.node.nodeinfo.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
 | 
			
		||||
          n.startAngle = (parseInt(n.node.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
 | 
			
		||||
        });
 | 
			
		||||
        this.redraw();
 | 
			
		||||
      },
 | 
			
		||||
@ -45,21 +45,17 @@ define(['leaflet', 'rbush', 'helper'],
 | 
			
		||||
          return tile;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var startDistance = 12;
 | 
			
		||||
        var startDistance = 10;
 | 
			
		||||
 | 
			
		||||
        ctx.beginPath();
 | 
			
		||||
        nodes.forEach(function (d) {
 | 
			
		||||
          var p = map.project([d.node.nodeinfo.location.latitude, d.node.nodeinfo.location.longitude]);
 | 
			
		||||
          var p = map.project([d.node.location.latitude, d.node.location.longitude]);
 | 
			
		||||
 | 
			
		||||
          p.x -= s.x;
 | 
			
		||||
          p.y -= s.y;
 | 
			
		||||
 | 
			
		||||
          helper.positionClients(ctx, p, d.startAngle, d.node.statistics.clients, startDistance);
 | 
			
		||||
          helper.positionClients(ctx, p, d.startAngle, d.node, startDistance);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        ctx.fillStyle = 'rgba(220, 0, 103, 0.7)';
 | 
			
		||||
        ctx.fill();
 | 
			
		||||
 | 
			
		||||
        return tile;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
  function (L, rbush, helper, moment) {
 | 
			
		||||
  function (L, RBush, helper, moment) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
 | 
			
		||||
    var groupOnline;
 | 
			
		||||
@ -35,14 +35,14 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
      return function (d) {
 | 
			
		||||
        var font = fontSize + 'px ' + bodyStyle.fontFamily;
 | 
			
		||||
        return {
 | 
			
		||||
          position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
 | 
			
		||||
          label: d.nodeinfo.hostname,
 | 
			
		||||
          position: L.latLng(d.location.latitude, d.location.longitude),
 | 
			
		||||
          label: d.hostname,
 | 
			
		||||
          offset: offset,
 | 
			
		||||
          fillStyle: fillStyle,
 | 
			
		||||
          height: fontSize * 1.2,
 | 
			
		||||
          font: font,
 | 
			
		||||
          stroke: stroke,
 | 
			
		||||
          width: measureText(font, d.nodeinfo.hostname).width
 | 
			
		||||
          width: measureText(font, d.hostname).width
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
@ -76,33 +76,33 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
      return { minX: x, minY: y, maxX: x + width, maxY: y + height };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function mkMarker(dict, iconFunc, router) {
 | 
			
		||||
    function mkMarker(dict, iconFunc) {
 | 
			
		||||
      return function (d) {
 | 
			
		||||
        var m = L.circleMarker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], iconFunc(d));
 | 
			
		||||
        var m = L.circleMarker([d.location.latitude, d.location.longitude], iconFunc(d));
 | 
			
		||||
 | 
			
		||||
        m.resetStyle = function resetStyle() {
 | 
			
		||||
          m.setStyle(iconFunc(d));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        m.on('click', function () {
 | 
			
		||||
          router.fullUrl({ node: d.nodeinfo.node_id });
 | 
			
		||||
          router.fullUrl({ node: d.node_id });
 | 
			
		||||
        });
 | 
			
		||||
        m.bindTooltip(d.nodeinfo.hostname);
 | 
			
		||||
        m.bindTooltip(helper.escape(d.hostname));
 | 
			
		||||
 | 
			
		||||
        dict[d.nodeinfo.node_id] = m;
 | 
			
		||||
        dict[d.node_id] = m;
 | 
			
		||||
 | 
			
		||||
        return m;
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addLinksToMap(dict, linkScale, graph, router) {
 | 
			
		||||
    function addLinksToMap(dict, linkScale, graph) {
 | 
			
		||||
      graph = graph.filter(function (d) {
 | 
			
		||||
        return 'distance' in d && !d.vpn;
 | 
			
		||||
        return 'distance' in d && d.type.indexOf('vpn') !== 0;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return graph.map(function (d) {
 | 
			
		||||
        var opts = {
 | 
			
		||||
          color: linkScale(1 / d.tq),
 | 
			
		||||
          color: linkScale((d.source_tq + d.target_tq) / 2),
 | 
			
		||||
          weight: 4,
 | 
			
		||||
          opacity: 0.5,
 | 
			
		||||
          dashArray: 'none'
 | 
			
		||||
@ -114,7 +114,9 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
          line.setStyle(opts);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        line.bindTooltip(d.source.node.nodeinfo.hostname + ' – ' + d.target.node.nodeinfo.hostname + '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d) + '</strong>');
 | 
			
		||||
        line.bindTooltip(helper.escape(d.source.hostname + ' – ' + d.target.hostname) +
 | 
			
		||||
          '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq) + '<br>' + d.type + '</strong>');
 | 
			
		||||
 | 
			
		||||
        line.on('click', function () {
 | 
			
		||||
          router.fullUrl({ link: d.id });
 | 
			
		||||
        });
 | 
			
		||||
@ -125,7 +127,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getIcon(config, color) {
 | 
			
		||||
    function getIcon(color) {
 | 
			
		||||
      return Object.assign({}, config.icon.base, config.icon[color]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -136,12 +138,12 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
          this.prepareLabels();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      setData: function (data, map, nodeDict, linkDict, linkScale, router, config) {
 | 
			
		||||
        var iconOnline = getIcon(config, 'online');
 | 
			
		||||
        var iconOffline = getIcon(config, 'offline');
 | 
			
		||||
        var iconLost = getIcon(config, 'lost');
 | 
			
		||||
        var iconAlert = getIcon(config, 'alert');
 | 
			
		||||
        var iconNew = getIcon(config, 'new');
 | 
			
		||||
      setData: function (data, map, nodeDict, linkDict, linkScale) {
 | 
			
		||||
        var iconOnline = getIcon('online');
 | 
			
		||||
        var iconOffline = getIcon('offline');
 | 
			
		||||
        var iconLost = getIcon('lost');
 | 
			
		||||
        var iconAlert = getIcon('alert');
 | 
			
		||||
        var iconNew = getIcon('new');
 | 
			
		||||
        // Check if init or data is already set
 | 
			
		||||
        if (groupLines) {
 | 
			
		||||
          groupOffline.clearLayers();
 | 
			
		||||
@ -151,38 +153,36 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
          groupLines.clearLayers();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router);
 | 
			
		||||
        var lines = addLinksToMap(linkDict, linkScale, data.links);
 | 
			
		||||
        groupLines = L.featureGroup(lines).addTo(map);
 | 
			
		||||
 | 
			
		||||
        var nodesOnline = helper.subtract(data.nodes.all.filter(helper.online), data.nodes.new);
 | 
			
		||||
        var nodesOffline = helper.subtract(data.nodes.all.filter(helper.offline), data.nodes.lost);
 | 
			
		||||
        var nodesOnline = helper.subtract(data.nodes.online, data.nodes.new).filter(helper.hasLocation);
 | 
			
		||||
        var nodesOffline = helper.subtract(data.nodes.offline, data.nodes.lost).filter(helper.hasLocation);
 | 
			
		||||
        var nodesNew = data.nodes.new.filter(helper.hasLocation);
 | 
			
		||||
        var nodesLost = data.nodes.lost.filter(helper.hasLocation);
 | 
			
		||||
 | 
			
		||||
        var markersOnline = nodesOnline.filter(helper.hasLocation)
 | 
			
		||||
          .map(mkMarker(nodeDict, function () {
 | 
			
		||||
            return iconOnline;
 | 
			
		||||
          }, router));
 | 
			
		||||
        var markersOnline = nodesOnline.map(mkMarker(nodeDict, function () {
 | 
			
		||||
          return iconOnline;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        var markersOffline = nodesOffline.filter(helper.hasLocation)
 | 
			
		||||
          .map(mkMarker(nodeDict, function () {
 | 
			
		||||
            return iconOffline;
 | 
			
		||||
          }, router));
 | 
			
		||||
        var markersOffline = nodesOffline.map(mkMarker(nodeDict, function () {
 | 
			
		||||
          return iconOffline;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        var markersNew = data.nodes.new.filter(helper.hasLocation)
 | 
			
		||||
          .map(mkMarker(nodeDict, function () {
 | 
			
		||||
            return iconNew;
 | 
			
		||||
          }, router));
 | 
			
		||||
        var markersNew = nodesNew.map(mkMarker(nodeDict, function () {
 | 
			
		||||
          return iconNew;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        var markersLost = data.nodes.lost.filter(helper.hasLocation)
 | 
			
		||||
          .map(mkMarker(nodeDict, function (d) {
 | 
			
		||||
            if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAgeAlert, 'days'))) {
 | 
			
		||||
              return iconAlert;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAge, 'days'))) {
 | 
			
		||||
              return iconLost;
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
          }, router));
 | 
			
		||||
        var markersLost = nodesLost.map(mkMarker(nodeDict, function (d) {
 | 
			
		||||
          var age = moment(data.now).diff(d.lastseen, 'days', true);
 | 
			
		||||
          if (age <= config.maxAgeAlert) {
 | 
			
		||||
            return iconAlert;
 | 
			
		||||
          }
 | 
			
		||||
          if (age <= config.maxAge) {
 | 
			
		||||
            return iconLost;
 | 
			
		||||
          }
 | 
			
		||||
          return null;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        groupOffline = L.featureGroup(markersOffline).addTo(map);
 | 
			
		||||
        groupLost = L.featureGroup(markersLost).addTo(map);
 | 
			
		||||
@ -190,10 +190,10 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
        groupNew = L.featureGroup(markersNew).addTo(map);
 | 
			
		||||
 | 
			
		||||
        this.data = {
 | 
			
		||||
          online: nodesOnline.filter(helper.hasLocation),
 | 
			
		||||
          offline: nodesOffline.filter(helper.hasLocation),
 | 
			
		||||
          new: data.nodes.new.filter(helper.hasLocation),
 | 
			
		||||
          lost: data.nodes.lost.filter(helper.hasLocation)
 | 
			
		||||
          online: nodesOnline,
 | 
			
		||||
          offline: nodesOffline,
 | 
			
		||||
          new: nodesNew,
 | 
			
		||||
          lost: nodesLost
 | 
			
		||||
        };
 | 
			
		||||
        this.updateLayer();
 | 
			
		||||
      },
 | 
			
		||||
@ -214,9 +214,9 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
        // - color (string)
 | 
			
		||||
 | 
			
		||||
        var labelsOnline = d.online.map(prepareLabel(null, 11, 8, true));
 | 
			
		||||
        var labelsOffline = d.offline.map(prepareLabel('rgba(212, 62, 42, 0.9)', 9, 5, false));
 | 
			
		||||
        var labelsNew = d.new.map(prepareLabel('rgba(48, 99, 20, 0.9)', 11, 8, true));
 | 
			
		||||
        var labelsLost = d.lost.map(prepareLabel('rgba(212, 62, 42, 0.9)', 11, 8, true));
 | 
			
		||||
        var labelsOffline = d.offline.map(prepareLabel(config.icon.offline.color, 9, 5, false));
 | 
			
		||||
        var labelsNew = d.new.map(prepareLabel(config.map.labelNewColor, 11, 8, true));
 | 
			
		||||
        var labelsLost = d.lost.map(prepareLabel(config.icon.lost.color, 11, 8, true));
 | 
			
		||||
 | 
			
		||||
        var labels = []
 | 
			
		||||
          .concat(labelsNew)
 | 
			
		||||
@ -239,7 +239,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (var z = minZoom; z <= maxZoom; z++) {
 | 
			
		||||
          trees[z] = rbush(9);
 | 
			
		||||
          trees[z] = new RBush(9);
 | 
			
		||||
          trees[z].load(labels.map(nodeToRect(z)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -291,7 +291,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 | 
			
		||||
          }).sort().reverse()[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.labels = rbush(9);
 | 
			
		||||
        this.labels = new RBush(9);
 | 
			
		||||
        this.labels.load(labels.map(mapRTree));
 | 
			
		||||
 | 
			
		||||
        this.redraw();
 | 
			
		||||
 | 
			
		||||
@ -2,39 +2,10 @@ define(['leaflet'], function (L) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  return L.CircleMarker.extend({
 | 
			
		||||
    outerCircle: {
 | 
			
		||||
      stroke: false,
 | 
			
		||||
      color: '#4285F4',
 | 
			
		||||
      opacity: 1,
 | 
			
		||||
      fillOpacity: 0.3,
 | 
			
		||||
      clickable: false,
 | 
			
		||||
      radius: 16
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    innerCircle: {
 | 
			
		||||
      stroke: true,
 | 
			
		||||
      color: '#ffffff',
 | 
			
		||||
      fillColor: '#4285F4',
 | 
			
		||||
      weight: 1.5,
 | 
			
		||||
      clickable: false,
 | 
			
		||||
      opacity: 1,
 | 
			
		||||
      fillOpacity: 1,
 | 
			
		||||
      radius: 7
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    accuracyCircle: {
 | 
			
		||||
      stroke: true,
 | 
			
		||||
      color: '#4285F4',
 | 
			
		||||
      weight: 1,
 | 
			
		||||
      clickable: false,
 | 
			
		||||
      opacity: 0.7,
 | 
			
		||||
      fillOpacity: 0.2
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    initialize: function (latlng) {
 | 
			
		||||
      this.accuracyCircle = L.circle(latlng, 0, this.accuracyCircle);
 | 
			
		||||
      this.outerCircle = L.circleMarker(latlng, this.outerCircle);
 | 
			
		||||
      L.CircleMarker.prototype.initialize.call(this, latlng, this.innerCircle);
 | 
			
		||||
      this.accuracyCircle = L.circle(latlng, 0, config.locate.accuracyCircle);
 | 
			
		||||
      this.outerCircle = L.circleMarker(latlng, config.locate.outerCircle);
 | 
			
		||||
      L.CircleMarker.prototype.initialize.call(this, latlng, config.locate.innerCircle);
 | 
			
		||||
 | 
			
		||||
      this.on('remove', function () {
 | 
			
		||||
        this._map.removeLayer(this.accuracyCircle);
 | 
			
		||||
 | 
			
		||||
@ -2,28 +2,18 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  V = V.default;
 | 
			
		||||
 | 
			
		||||
  function getUptime(now, d) {
 | 
			
		||||
    if (d.flags.online && 'uptime' in d.statistics) {
 | 
			
		||||
      return Math.round(d.statistics.uptime);
 | 
			
		||||
    } else if (!d.flags.online && 'lastseen' in d) {
 | 
			
		||||
      return Math.round(-(now.unix() - d.lastseen.unix()));
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function showUptime(uptime) {
 | 
			
		||||
    var s = '';
 | 
			
		||||
    uptime /= 3600;
 | 
			
		||||
 | 
			
		||||
    if (uptime !== undefined) {
 | 
			
		||||
      if (Math.abs(uptime) >= 24) {
 | 
			
		||||
        s = Math.round(uptime / 24) + 'd';
 | 
			
		||||
      } else {
 | 
			
		||||
        s = Math.round(uptime) + 'h';
 | 
			
		||||
      }
 | 
			
		||||
    // 1000ms are 1 second and 60 second are 1min: 60 * 1000 =  60000
 | 
			
		||||
    var s = uptime / 60000;
 | 
			
		||||
    if (Math.abs(s) < 60) {
 | 
			
		||||
      return Math.round(s) + ' m';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return s;
 | 
			
		||||
    s /= 60;
 | 
			
		||||
    if (Math.abs(s) < 24) {
 | 
			
		||||
      return Math.round(s) + ' h';
 | 
			
		||||
    }
 | 
			
		||||
    s /= 24;
 | 
			
		||||
    return Math.round(s) + ' d';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var headings = [{
 | 
			
		||||
@ -31,7 +21,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
  }, {
 | 
			
		||||
    name: 'node.nodes',
 | 
			
		||||
    sort: function (a, b) {
 | 
			
		||||
      return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname);
 | 
			
		||||
      return a.hostname.localeCompare(b.hostname);
 | 
			
		||||
    },
 | 
			
		||||
    reverse: false
 | 
			
		||||
  }, {
 | 
			
		||||
@ -52,40 +42,36 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
    name: 'node.clients',
 | 
			
		||||
    class: 'ion-people',
 | 
			
		||||
    sort: function (a, b) {
 | 
			
		||||
      return ('clients' in a.statistics ? a.statistics.clients : -1) -
 | 
			
		||||
        ('clients' in b.statistics ? b.statistics.clients : -1);
 | 
			
		||||
      return a.clients - b.clients;
 | 
			
		||||
    },
 | 
			
		||||
    reverse: true
 | 
			
		||||
  }];
 | 
			
		||||
 | 
			
		||||
  return function (router) {
 | 
			
		||||
  return function () {
 | 
			
		||||
    function renderRow(d) {
 | 
			
		||||
      var td0Content = [];
 | 
			
		||||
      var td1Content = [];
 | 
			
		||||
      var aClass = ['hostname', d.flags.online ? 'online' : 'offline'];
 | 
			
		||||
 | 
			
		||||
      td1Content.push(V.h('a', {
 | 
			
		||||
        props: {
 | 
			
		||||
          className: aClass.join(' '),
 | 
			
		||||
          href: router.generateLink({ node: d.nodeinfo.node_id })
 | 
			
		||||
        }, on: {
 | 
			
		||||
          click: function (e) {
 | 
			
		||||
            router.fullUrl({ node: d.nodeinfo.node_id }, e);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }, d.nodeinfo.hostname));
 | 
			
		||||
 | 
			
		||||
      var td0Content = '';
 | 
			
		||||
      if (helper.hasLocation(d)) {
 | 
			
		||||
        td0Content.push(V.h('span', { props: { className: 'icon ion-location' } }));
 | 
			
		||||
        td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var td0 = V.h('td', td0Content);
 | 
			
		||||
      var td1 = V.h('td', td1Content);
 | 
			
		||||
      var td2 = V.h('td', showUptime(d.uptime));
 | 
			
		||||
      var td3 = V.h('td', d.neighbours.length);
 | 
			
		||||
      var td4 = V.h('td', Number('clients' in d.statistics ? d.statistics.clients : 0).toFixed(0));
 | 
			
		||||
      var td1Content = V.h('a', {
 | 
			
		||||
        props: {
 | 
			
		||||
          className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
 | 
			
		||||
          href: router.generateLink({ node: d.node_id })
 | 
			
		||||
        }, on: {
 | 
			
		||||
          click: function (e) {
 | 
			
		||||
            router.fullUrl({ node: d.node_id }, e);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }, d.hostname);
 | 
			
		||||
 | 
			
		||||
      return V.h('tr', [td0, td1, td2, td3, td4]);
 | 
			
		||||
      return V.h('tr', [
 | 
			
		||||
        V.h('td', td0Content),
 | 
			
		||||
        V.h('td', td1Content),
 | 
			
		||||
        V.h('td', showUptime(d.uptime)),
 | 
			
		||||
        V.h('td', d.neighbours.length),
 | 
			
		||||
        V.h('td', d.clients)
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var table = new SortTable(headings, 1, renderRow);
 | 
			
		||||
@ -101,8 +87,11 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
 | 
			
		||||
    this.setData = function setData(d) {
 | 
			
		||||
      var data = d.nodes.all.map(function (e) {
 | 
			
		||||
        var n = Object.create(e);
 | 
			
		||||
        n.uptime = getUptime(d.now, e);
 | 
			
		||||
        n.neighbours = e.neighbours;
 | 
			
		||||
        if (e.is_online) {
 | 
			
		||||
          n.uptime = d.now - new Date(e.uptime).getTime();
 | 
			
		||||
        } else {
 | 
			
		||||
          n.uptime = e.lastseen - d.now;
 | 
			
		||||
        }
 | 
			
		||||
        return n;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,21 @@
 | 
			
		||||
define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
 | 
			
		||||
  function (d3Interpolate, V, Filter, helper) {
 | 
			
		||||
define(['d3-interpolate', 'snabbdom', 'utils/version', 'filters/genericnode', 'helper'],
 | 
			
		||||
  function (d3Interpolate, V, versionCompare, Filter, helper) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
    V = V.default;
 | 
			
		||||
 | 
			
		||||
    return function (config, filterManager) {
 | 
			
		||||
    return function (filterManager) {
 | 
			
		||||
      var self = this;
 | 
			
		||||
      var scale = d3Interpolate.interpolate('#770038', '#dc0067');
 | 
			
		||||
      V = V.default;
 | 
			
		||||
      var scale = d3Interpolate.interpolate(config.forceGraph.tqFrom, config.forceGraph.tqTo);
 | 
			
		||||
      var time;
 | 
			
		||||
 | 
			
		||||
      var statusTable;
 | 
			
		||||
      var fwTable;
 | 
			
		||||
      var hwTable;
 | 
			
		||||
      var geoTable;
 | 
			
		||||
      var autoTable;
 | 
			
		||||
      var siteTable;
 | 
			
		||||
 | 
			
		||||
      function showStatGlobal(o) {
 | 
			
		||||
        return helper.showStat(o);
 | 
			
		||||
      }
 | 
			
		||||
      var gatewayTable;
 | 
			
		||||
      var gateway6Table;
 | 
			
		||||
      var domainTable;
 | 
			
		||||
 | 
			
		||||
      function count(nodes, key, f) {
 | 
			
		||||
        var dict = {};
 | 
			
		||||
@ -61,14 +60,13 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
 | 
			
		||||
 | 
			
		||||
          var filter = new Filter(_.t(name), d[2], d[0], d[3]);
 | 
			
		||||
 | 
			
		||||
          var a = V.h('a', { props: { href: '#' }, on: { click: addFilter(filter) } }, d[0]);
 | 
			
		||||
          var a = V.h('a', { on: { click: addFilter(filter) } }, d[0]);
 | 
			
		||||
 | 
			
		||||
          var th = V.h('th', a);
 | 
			
		||||
          var td = V.h('td', V.h('span', {
 | 
			
		||||
            style: {
 | 
			
		||||
              width: Math.round(v * 100) + '%',
 | 
			
		||||
              backgroundColor: scale(v),
 | 
			
		||||
              color: 'white'
 | 
			
		||||
              width: 'calc(25px + ' + Math.round(v * 90) + '%)',
 | 
			
		||||
              backgroundColor: scale(v)
 | 
			
		||||
            }
 | 
			
		||||
          }, d[1].toFixed(0)));
 | 
			
		||||
 | 
			
		||||
@ -79,56 +77,53 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      self.setData = function setData(data) {
 | 
			
		||||
        var onlineNodes = data.nodes.all.filter(helper.online);
 | 
			
		||||
        var onlineNodes = data.nodes.online;
 | 
			
		||||
        var nodes = onlineNodes.concat(data.nodes.lost);
 | 
			
		||||
        var nodeDict = {};
 | 
			
		||||
        time = data.timestamp;
 | 
			
		||||
 | 
			
		||||
        data.nodes.all.forEach(function (d) {
 | 
			
		||||
          nodeDict[d.nodeinfo.node_id] = d;
 | 
			
		||||
        });
 | 
			
		||||
        function hostnameOfNodeID(nodeid) {
 | 
			
		||||
          var gateway = data.nodeDict[nodeid];
 | 
			
		||||
          if (gateway) {
 | 
			
		||||
            return gateway.hostname;
 | 
			
		||||
          }
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var statusDict = count(nodes, ['flags', 'online'], function (d) {
 | 
			
		||||
        var gatewayDict = count(nodes, ['gateway'], hostnameOfNodeID);
 | 
			
		||||
        var gateway6Dict = count(nodes, ['gateway6'], hostnameOfNodeID);
 | 
			
		||||
 | 
			
		||||
        var statusDict = count(nodes, ['is_online'], function (d) {
 | 
			
		||||
          return d ? 'online' : 'offline';
 | 
			
		||||
        });
 | 
			
		||||
        var fwDict = count(nodes, ['nodeinfo', 'software', 'firmware', 'release']);
 | 
			
		||||
        var hwDict = count(nodes, ['nodeinfo', 'hardware', 'model']);
 | 
			
		||||
        var geoDict = count(nodes, ['nodeinfo', 'location'], function (d) {
 | 
			
		||||
        var fwDict = count(nodes, ['firmware', 'release']);
 | 
			
		||||
        var hwDict = count(nodes, ['model']);
 | 
			
		||||
        var geoDict = count(nodes, ['location'], function (d) {
 | 
			
		||||
          return d && d.longitude && d.latitude ? _.t('yes') : _.t('no');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var autoDict = count(nodes, ['nodeinfo', 'software', 'autoupdater'], function (d) {
 | 
			
		||||
          if (d === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
          } else if (d.enabled) {
 | 
			
		||||
        var autoDict = count(nodes, ['autoupdater'], function (d) {
 | 
			
		||||
          if (d.enabled) {
 | 
			
		||||
            return d.branch;
 | 
			
		||||
          }
 | 
			
		||||
          return _.t('node.deactivated');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var siteDict = count(nodes, ['nodeinfo', 'system', 'site_code'], function (d) {
 | 
			
		||||
          var rt = d;
 | 
			
		||||
          if (config.siteNames) {
 | 
			
		||||
            config.siteNames.forEach(function (t) {
 | 
			
		||||
              if (d === t.site) {
 | 
			
		||||
                rt = t.name;
 | 
			
		||||
        var domainDict = count(nodes, ['domain'], function (d) {
 | 
			
		||||
          if (config.domainNames) {
 | 
			
		||||
            config.domainNames.some(function (t) {
 | 
			
		||||
              if (d === t.domain) {
 | 
			
		||||
                d = t.name;
 | 
			
		||||
                return true;
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          return rt;
 | 
			
		||||
          return d;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        statusTable = fillTable('node.status', statusTable, statusDict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
        fwTable = fillTable('node.firmware', fwTable, fwDict.sort(function (a, b) {
 | 
			
		||||
          if (b[0] < a[0]) {
 | 
			
		||||
            return -1;
 | 
			
		||||
          }
 | 
			
		||||
          if (b[0] > a[0]) {
 | 
			
		||||
            return 1;
 | 
			
		||||
          }
 | 
			
		||||
          return 0;
 | 
			
		||||
        }));
 | 
			
		||||
        fwTable = fillTable('node.firmware', fwTable, fwDict.sort(versionCompare));
 | 
			
		||||
        hwTable = fillTable('node.hardware', hwTable, hwDict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
@ -138,39 +133,54 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
 | 
			
		||||
        autoTable = fillTable('node.update', autoTable, autoDict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
        siteTable = fillTable('node.site', siteTable, siteDict.sort(function (a, b) {
 | 
			
		||||
        gatewayTable = fillTable('node.selectedGatewayIPv4', gatewayTable, gatewayDict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
        gateway6Table = fillTable('node.selectedGatewayIPv6', gateway6Table, gateway6Dict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
        domainTable = fillTable('node.domain', domainTable, domainDict.sort(function (a, b) {
 | 
			
		||||
          return b[1] - a[1];
 | 
			
		||||
        }));
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.render = function render(el) {
 | 
			
		||||
        var h2;
 | 
			
		||||
        self.renderSingle(el, 'node.status', statusTable);
 | 
			
		||||
        self.renderSingle(el, 'node.firmware', fwTable);
 | 
			
		||||
        self.renderSingle(el, 'node.hardware', hwTable);
 | 
			
		||||
        self.renderSingle(el, 'node.visible', geoTable);
 | 
			
		||||
        self.renderSingle(el, 'node.update', autoTable);
 | 
			
		||||
        self.renderSingle(el, 'node.site', siteTable);
 | 
			
		||||
        self.renderSingle(el, 'node.selectedGatewayIPv4', gatewayTable);
 | 
			
		||||
        self.renderSingle(el, 'node.selectedGatewayIPv6', gateway6Table);
 | 
			
		||||
        self.renderSingle(el, 'node.domain', domainTable);
 | 
			
		||||
 | 
			
		||||
        if (config.globalInfos) {
 | 
			
		||||
          var images = document.createElement('div');
 | 
			
		||||
          el.appendChild(images);
 | 
			
		||||
          var img = [];
 | 
			
		||||
          var subst = {
 | 
			
		||||
            '{TIME}': time,
 | 
			
		||||
            '{LOCALE}': _.locale()
 | 
			
		||||
          };
 | 
			
		||||
          config.globalInfos.forEach(function (globalInfo) {
 | 
			
		||||
            h2 = document.createElement('h2');
 | 
			
		||||
            h2.textContent = globalInfo.name;
 | 
			
		||||
            el.appendChild(h2);
 | 
			
		||||
            el.appendChild(showStatGlobal(globalInfo));
 | 
			
		||||
            img.push(V.h('h2', globalInfo.name));
 | 
			
		||||
            img.push(helper.showStat(V, globalInfo, subst));
 | 
			
		||||
          });
 | 
			
		||||
          V.patch(images, V.h('div', img));
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      self.renderSingle = function renderSingle(el, heading, table) {
 | 
			
		||||
        var h2 = document.createElement('h2');
 | 
			
		||||
        h2.classList.add('proportion-header');
 | 
			
		||||
        h2.textContent = _.t(heading);
 | 
			
		||||
        h2.onclick = function onclick() {
 | 
			
		||||
          table.elm.classList.toggle('hide');
 | 
			
		||||
        };
 | 
			
		||||
        el.appendChild(h2);
 | 
			
		||||
        el.appendChild(table.elm);
 | 
			
		||||
        if (table.children.length > 0) {
 | 
			
		||||
          var h2 = document.createElement('h2');
 | 
			
		||||
          h2.classList.add('proportion-header');
 | 
			
		||||
          h2.textContent = _.t(heading);
 | 
			
		||||
          h2.onclick = function onclick() {
 | 
			
		||||
            table.elm.classList.toggle('hide');
 | 
			
		||||
          };
 | 
			
		||||
          el.appendChild(h2);
 | 
			
		||||
          el.appendChild(table.elm);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      return self;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,13 @@ define(function () {
 | 
			
		||||
    el.appendChild(sidebar);
 | 
			
		||||
 | 
			
		||||
    var button = document.createElement('button');
 | 
			
		||||
    var visibility = new CustomEvent('visibility');
 | 
			
		||||
    sidebar.appendChild(button);
 | 
			
		||||
 | 
			
		||||
    button.classList.add('sidebarhandle', 'shadow');
 | 
			
		||||
    button.classList.add('sidebarhandle');
 | 
			
		||||
    button.setAttribute('aria-label', _.t('sidebar.toggle'));
 | 
			
		||||
    button.onclick = function onclick() {
 | 
			
		||||
      button.dispatchEvent(visibility);
 | 
			
		||||
      sidebar.classList.toggle('hidden');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -27,7 +30,7 @@ define(function () {
 | 
			
		||||
    sidebar.appendChild(container);
 | 
			
		||||
 | 
			
		||||
    self.getWidth = function getWidth() {
 | 
			
		||||
      if (gridBreakpoints.lg[0] > window.innerWidth) {
 | 
			
		||||
      if (gridBreakpoints.lg[0] > window.innerWidth || sidebar.classList.contains('hidden')) {
 | 
			
		||||
        return 0;
 | 
			
		||||
      } else if (gridBreakpoints.xl[0] > window.innerWidth) {
 | 
			
		||||
        return gridBreakpoints.lg[1];
 | 
			
		||||
@ -54,6 +57,7 @@ define(function () {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    self.container = sidebar;
 | 
			
		||||
    self.button = button;
 | 
			
		||||
 | 
			
		||||
    return self;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  V = V.default;
 | 
			
		||||
 | 
			
		||||
  return function (nodes, field, router, title) {
 | 
			
		||||
  return function (nodes, field, title) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    var el;
 | 
			
		||||
    var tbody;
 | 
			
		||||
@ -34,32 +34,27 @@ define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var items = list.map(function (d) {
 | 
			
		||||
        var time = moment(d[field]).from(data.now);
 | 
			
		||||
        var td0Content = [];
 | 
			
		||||
        var td1Content = [];
 | 
			
		||||
 | 
			
		||||
        var aClass = ['hostname', d.flags.online ? 'online' : 'offline'];
 | 
			
		||||
 | 
			
		||||
        td1Content.push(V.h('a', {
 | 
			
		||||
          props: {
 | 
			
		||||
            className: aClass.join(' '),
 | 
			
		||||
            href: router.generateLink({ node: d.nodeinfo.node_id })
 | 
			
		||||
          }, on: {
 | 
			
		||||
            click: function (e) {
 | 
			
		||||
              router.fullUrl({ node: d.nodeinfo.node_id }, e);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, d.nodeinfo.hostname));
 | 
			
		||||
 | 
			
		||||
        var td0Content = '';
 | 
			
		||||
        if (helper.hasLocation(d)) {
 | 
			
		||||
          td0Content.push(V.h('span', { props: { className: 'icon ion-location' } }));
 | 
			
		||||
          td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var td0 = V.h('td', td0Content);
 | 
			
		||||
        var td1 = V.h('td', td1Content);
 | 
			
		||||
        var td2 = V.h('td', time);
 | 
			
		||||
        var td1Content = V.h('a', {
 | 
			
		||||
          props: {
 | 
			
		||||
            className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
 | 
			
		||||
            href: router.generateLink({ node: d.node_id })
 | 
			
		||||
          }, on: {
 | 
			
		||||
            click: function (e) {
 | 
			
		||||
              router.fullUrl({ node: d.node_id }, e);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, d.hostname);
 | 
			
		||||
 | 
			
		||||
        return V.h('tr', [td0, td1, td2]);
 | 
			
		||||
        return V.h('tr', [
 | 
			
		||||
          V.h('td', td0Content),
 | 
			
		||||
          V.h('td', td1Content),
 | 
			
		||||
          V.h('td', moment(d[field]).from(data.now))
 | 
			
		||||
        ]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      var tbodyNew = V.h('tbody', items);
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
define(function () {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  return function (config) {
 | 
			
		||||
  return function () {
 | 
			
		||||
    function setTitle(d) {
 | 
			
		||||
      var title = [config.siteName];
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,11 @@ define(function () {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.gotoNode = function gotoNode(d) {
 | 
			
		||||
      setTitle(d.nodeinfo.hostname);
 | 
			
		||||
      setTitle(d.hostname);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.gotoLink = function gotoLink(d) {
 | 
			
		||||
      setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' \u21D4 ' + d.target.node.nodeinfo.hostname);
 | 
			
		||||
      setTitle(d[0].source.hostname + ' \u21D4 ' + d[0].target.hostname);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.gotoLocation = function gotoLocation() {
 | 
			
		||||
 | 
			
		||||
@ -27,9 +27,9 @@ define({
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  sortByKey: function sortByKey(key, d) {
 | 
			
		||||
    return d.slice().sort(function (a, b) {
 | 
			
		||||
      return a[key] - b[key];
 | 
			
		||||
    }).reverse();
 | 
			
		||||
    return d.sort(function (a, b) {
 | 
			
		||||
      return b[key] - a[key];
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  limit: function limit(key, m, d) {
 | 
			
		||||
@ -48,10 +48,6 @@ define({
 | 
			
		||||
    return 1;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  trueDefault: function trueDefault(d) {
 | 
			
		||||
    return d === undefined ? true : d;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dictGet: function dictGet(dict, key) {
 | 
			
		||||
    var k = key.shift();
 | 
			
		||||
 | 
			
		||||
@ -76,31 +72,21 @@ define({
 | 
			
		||||
    return s;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /* Helpers working with nodes */
 | 
			
		||||
 | 
			
		||||
  offline: function offline(d) {
 | 
			
		||||
    return !d.flags.online;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  online: function online(d) {
 | 
			
		||||
    return d.flags.online;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  hasLocation: function hasLocation(d) {
 | 
			
		||||
    return 'location' in d.nodeinfo &&
 | 
			
		||||
      Math.abs(d.nodeinfo.location.latitude) < 90 &&
 | 
			
		||||
      Math.abs(d.nodeinfo.location.longitude) < 180;
 | 
			
		||||
    return 'location' in d &&
 | 
			
		||||
      Math.abs(d.location.latitude) < 90 &&
 | 
			
		||||
      Math.abs(d.location.longitude) < 180;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  subtract: function subtract(a, b) {
 | 
			
		||||
    var ids = {};
 | 
			
		||||
 | 
			
		||||
    b.forEach(function (d) {
 | 
			
		||||
      ids[d.nodeinfo.node_id] = true;
 | 
			
		||||
      ids[d.node_id] = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return a.filter(function (d) {
 | 
			
		||||
      return !(d.nodeinfo.node_id in ids);
 | 
			
		||||
      return !ids[d.node_id];
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -115,59 +101,35 @@ define({
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  showTq: function showTq(d) {
 | 
			
		||||
    return (1 / d.tq * 100).toFixed(0) + '%';
 | 
			
		||||
    return (d * 100).toFixed(0) + '%';
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  attributeEntry: function attributeEntry(el, label, value) {
 | 
			
		||||
    if (value === null || value === undefined) {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var tr = document.createElement('tr');
 | 
			
		||||
    var th = document.createElement('th');
 | 
			
		||||
    th.textContent = _.t(label);
 | 
			
		||||
    tr.appendChild(th);
 | 
			
		||||
 | 
			
		||||
    var td = document.createElement('td');
 | 
			
		||||
 | 
			
		||||
    if (typeof value === 'function') {
 | 
			
		||||
      value(td);
 | 
			
		||||
    } else {
 | 
			
		||||
      td.appendChild(document.createTextNode(value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tr.appendChild(td);
 | 
			
		||||
 | 
			
		||||
    el.appendChild(tr);
 | 
			
		||||
 | 
			
		||||
    return td;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  showStat: function showStat(o, subst) {
 | 
			
		||||
    var content;
 | 
			
		||||
    subst = typeof subst !== 'undefined' ? subst : {};
 | 
			
		||||
 | 
			
		||||
    content = document.createElement('img');
 | 
			
		||||
    content.src = require('helper').listReplace(o.image, subst);
 | 
			
		||||
 | 
			
		||||
    var p = document.createElement('p');
 | 
			
		||||
 | 
			
		||||
    if (o.href) {
 | 
			
		||||
      var link = document.createElement('a');
 | 
			
		||||
      link.target = '_blank';
 | 
			
		||||
      link.href = require('helper').listReplace(o.href, subst);
 | 
			
		||||
      link.appendChild(content);
 | 
			
		||||
 | 
			
		||||
      if (o.title) {
 | 
			
		||||
        link.title = require('helper').listReplace(o.title, subst);
 | 
			
		||||
  attributeEntry: function attributeEntry(V, children, label, value) {
 | 
			
		||||
    if (value !== undefined) {
 | 
			
		||||
      if (typeof value !== 'object') {
 | 
			
		||||
        value = V.h('td', value);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      p.appendChild(link);
 | 
			
		||||
    } else {
 | 
			
		||||
      p.appendChild(content);
 | 
			
		||||
      children.push(V.h('tr', [
 | 
			
		||||
        V.h('th', _.t(label)),
 | 
			
		||||
        value
 | 
			
		||||
      ]));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  showStat: function showStat(V, o, subst) {
 | 
			
		||||
    var content = V.h('img', { attrs: { src: require('helper').listReplace(o.image, subst) } });
 | 
			
		||||
 | 
			
		||||
    return p;
 | 
			
		||||
    if (o.href) {
 | 
			
		||||
      return V.h('p', V.h('a', {
 | 
			
		||||
        attrs:
 | 
			
		||||
          {
 | 
			
		||||
            href: require('helper').listReplace(o.href, subst),
 | 
			
		||||
            target: '_blank',
 | 
			
		||||
            title: require('helper').listReplace(o.title, subst)
 | 
			
		||||
          }
 | 
			
		||||
      }, content));
 | 
			
		||||
    }
 | 
			
		||||
    return V.h('p', content);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  getTileBBox: function getTileBBox(s, map, tileSize, margin) {
 | 
			
		||||
@ -176,20 +138,35 @@ define({
 | 
			
		||||
 | 
			
		||||
    return { minX: br.lat, minY: tl.lng, maxX: tl.lat, maxY: br.lng };
 | 
			
		||||
  },
 | 
			
		||||
  positionClients: function positionClients(ctx, p, startAngle, clients, startDistance) {
 | 
			
		||||
    if (clients === 0) {
 | 
			
		||||
  positionClients: function positionClients(ctx, p, startAngle, node, startDistance) {
 | 
			
		||||
    if (node.clients === 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var radius = 3;
 | 
			
		||||
    var a = 1.2;
 | 
			
		||||
    var mode = 0;
 | 
			
		||||
 | 
			
		||||
    for (var orbit = 0, i = 0; i < clients; orbit++) {
 | 
			
		||||
    ctx.beginPath();
 | 
			
		||||
    ctx.fillStyle = config.client.wifi24;
 | 
			
		||||
 | 
			
		||||
    for (var orbit = 0, i = 0; i < node.clients; orbit++) {
 | 
			
		||||
      var distance = startDistance + orbit * 2 * radius * a;
 | 
			
		||||
      var n = Math.floor((Math.PI * distance) / (a * radius));
 | 
			
		||||
      var delta = clients - i;
 | 
			
		||||
      var delta = node.clients - i;
 | 
			
		||||
 | 
			
		||||
      for (var j = 0; j < Math.min(delta, n); i++, j++) {
 | 
			
		||||
        if (mode !== 1 && i >= (node.clients_wifi24 + node.clients_wifi5)) {
 | 
			
		||||
          mode = 1;
 | 
			
		||||
          ctx.fill();
 | 
			
		||||
          ctx.beginPath();
 | 
			
		||||
          ctx.fillStyle = config.client.wifi5;
 | 
			
		||||
        } else if (mode === 0 && i >= node.clients_wifi24) {
 | 
			
		||||
          mode = 2;
 | 
			
		||||
          ctx.fill();
 | 
			
		||||
          ctx.beginPath();
 | 
			
		||||
          ctx.fillStyle = config.client.other;
 | 
			
		||||
        }
 | 
			
		||||
        var angle = 2 * Math.PI / n * j;
 | 
			
		||||
        var x = p.x + distance * Math.cos(angle + startAngle);
 | 
			
		||||
        var y = p.y + distance * Math.sin(angle + startAngle);
 | 
			
		||||
@ -198,5 +175,32 @@ define({
 | 
			
		||||
        ctx.arc(x, y, radius, 0, 2 * Math.PI);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    ctx.fill();
 | 
			
		||||
  },
 | 
			
		||||
  fullscreen: function fullscreen(btn) {
 | 
			
		||||
    if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement) {
 | 
			
		||||
      var fel = document.firstElementChild;
 | 
			
		||||
      var func = fel.requestFullscreen
 | 
			
		||||
        || fel.webkitRequestFullScreen
 | 
			
		||||
        || fel.mozRequestFullScreen;
 | 
			
		||||
      func.call(fel);
 | 
			
		||||
      btn.classList.remove('ion-full-enter');
 | 
			
		||||
      btn.classList.add('ion-full-exit');
 | 
			
		||||
    } else {
 | 
			
		||||
      func = document.exitFullscreen
 | 
			
		||||
        || document.webkitExitFullscreen
 | 
			
		||||
        || document.mozCancelFullScreen;
 | 
			
		||||
      if (func) {
 | 
			
		||||
        func.call(document);
 | 
			
		||||
        btn.classList.remove('ion-full-exit');
 | 
			
		||||
        btn.classList.add('ion-full-enter');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  escape: function escape(string) {
 | 
			
		||||
    return string.replace(/</g, '<')
 | 
			
		||||
      .replace(/>/g, '>')
 | 
			
		||||
      .replace(/"/g, '"')
 | 
			
		||||
      .replace(/'/g, ''');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,12 @@
 | 
			
		||||
define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  return function (config) {
 | 
			
		||||
  return function () {
 | 
			
		||||
    var router;
 | 
			
		||||
 | 
			
		||||
    function languageSelect(el) {
 | 
			
		||||
      var select = document.createElement('select');
 | 
			
		||||
      select.className = 'language-switch';
 | 
			
		||||
      select.setAttribute('aria-label', 'Language');
 | 
			
		||||
      select.addEventListener('change', setSelectLocale);
 | 
			
		||||
      el.appendChild(select);
 | 
			
		||||
 | 
			
		||||
@ -20,13 +21,8 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
 | 
			
		||||
      router.fullUrl({ lang: event.target.value }, false, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setLocale(lang) {
 | 
			
		||||
      localStorage.setItem('language', getLocale(lang));
 | 
			
		||||
      location.reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getLocale(input) {
 | 
			
		||||
      var language = input || localStorage.getItem('language') || navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage;
 | 
			
		||||
      var language = input || navigator.languages && navigator.languages[0] || navigator.language;
 | 
			
		||||
      var locale = config.supportedLocale[0];
 | 
			
		||||
      config.supportedLocale.some(function (item) {
 | 
			
		||||
        if (language.indexOf(item) !== -1) {
 | 
			
		||||
@ -59,6 +55,7 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
 | 
			
		||||
 | 
			
		||||
    function init(r) {
 | 
			
		||||
      router = r;
 | 
			
		||||
      /** global: _ */
 | 
			
		||||
      window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true });
 | 
			
		||||
      helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
 | 
			
		||||
      document.querySelector('html').setAttribute('lang', _.locale());
 | 
			
		||||
@ -67,7 +64,6 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
 | 
			
		||||
    return {
 | 
			
		||||
      init: init,
 | 
			
		||||
      getLocale: getLocale,
 | 
			
		||||
      setLocale: setLocale,
 | 
			
		||||
      languageSelect: languageSelect
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ define(function () {
 | 
			
		||||
  var self = {};
 | 
			
		||||
 | 
			
		||||
  self.distance = function distance(a, b) {
 | 
			
		||||
    return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
 | 
			
		||||
    return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.distancePoint = function distancePoint(a, b) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										144
									
								
								lib/utils/node.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,144 @@
 | 
			
		||||
define(['snabbdom', 'helper', 'moment'], function (V, helper, moment) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  V = V.default;
 | 
			
		||||
 | 
			
		||||
  var self = {};
 | 
			
		||||
 | 
			
		||||
  function showBar(v, width, warning) {
 | 
			
		||||
    return V.h('span',
 | 
			
		||||
      { props: { className: 'bar' + (warning ? ' warning' : '') } },
 | 
			
		||||
      [
 | 
			
		||||
        V.h('span',
 | 
			
		||||
          {
 | 
			
		||||
            style: { width: (width * 100) + '%' }
 | 
			
		||||
          }),
 | 
			
		||||
        V.h('label', v)
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  self.showStatus = function showStatus(d) {
 | 
			
		||||
    return V.h('td',
 | 
			
		||||
      { props: { className: d.is_online ? 'online' : 'offline' } },
 | 
			
		||||
      _.t((d.is_online ? 'node.lastOnline' : 'node.lastOffline'), {
 | 
			
		||||
        time: d.lastseen.fromNow(),
 | 
			
		||||
        date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
 | 
			
		||||
      }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showGeoURI = function showGeoURI(d) {
 | 
			
		||||
    if (!helper.hasLocation(d)) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return V.h('td',
 | 
			
		||||
      V.h('a',
 | 
			
		||||
        { props: { href: 'geo:' + d.location.latitude + ',' + d.location.longitude } },
 | 
			
		||||
        Number(d.location.latitude.toFixed(6)) + ', ' + Number(d.location.longitude.toFixed(6))
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showGateway = function showGateway(d) {
 | 
			
		||||
    return d.is_gateway ? _.t('yes') : undefined;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showFirmware = function showFirmware(d) {
 | 
			
		||||
    return [
 | 
			
		||||
      helper.dictGet(d, ['firmware', 'release']),
 | 
			
		||||
      helper.dictGet(d, ['firmware', 'base'])
 | 
			
		||||
    ].filter(function (n) {
 | 
			
		||||
      return n !== null;
 | 
			
		||||
    }).join(' / ') || undefined;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showUptime = function showUptime(d) {
 | 
			
		||||
    return moment.utc(d.uptime).local().fromNow(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showFirstSeen = function showFirstSeen(d) {
 | 
			
		||||
    return d.firstseen.fromNow(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showLoad = function showLoad(d) {
 | 
			
		||||
    return showBar(d.loadavg.toFixed(2), d.loadavg / (d.nproc || 1), d.loadavg >= d.nproc);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showRAM = function showRAM(d) {
 | 
			
		||||
    return showBar(Math.round(d.memory_usage * 100) + ' %', d.memory_usage, d.memory_usage >= 0.8);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showDomain = function showDomain(d) {
 | 
			
		||||
    var rt = d.domain;
 | 
			
		||||
    if (config.domainNames) {
 | 
			
		||||
      config.domainNames.some(function (t) {
 | 
			
		||||
        if (rt === t.domain) {
 | 
			
		||||
          rt = t.name;
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return rt;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showClients = function showClients(d) {
 | 
			
		||||
    if (!d.is_online) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var clients = [
 | 
			
		||||
      V.h('span', [
 | 
			
		||||
        d.clients > 0 ? d.clients : _.t('none'),
 | 
			
		||||
        V.h('br'),
 | 
			
		||||
        V.h('i', { props: { className: 'ion-people', title: _.t('node.clients') } })
 | 
			
		||||
      ]),
 | 
			
		||||
      V.h('span',
 | 
			
		||||
        { props: { className: 'legend-24ghz' } },
 | 
			
		||||
        [
 | 
			
		||||
          d.clients_wifi24,
 | 
			
		||||
          V.h('br'),
 | 
			
		||||
          V.h('span', { props: { className: 'symbol', title: '2,4 GHz' } })
 | 
			
		||||
        ]),
 | 
			
		||||
      V.h('span',
 | 
			
		||||
        { props: { className: 'legend-5ghz' } },
 | 
			
		||||
        [
 | 
			
		||||
          d.clients_wifi5,
 | 
			
		||||
          V.h('br'),
 | 
			
		||||
          V.h('span', { props: { className: 'symbol', title: '5 GHz' } })
 | 
			
		||||
        ]),
 | 
			
		||||
      V.h('span',
 | 
			
		||||
        { props: { className: 'legend-others' } },
 | 
			
		||||
        [
 | 
			
		||||
          d.clients_other,
 | 
			
		||||
          V.h('br'),
 | 
			
		||||
          V.h('span', { props: { className: 'symbol', title: _.t('others') } })
 | 
			
		||||
        ])
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return V.h('td', { props: { className: 'clients' } }, clients);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showIPs = function showIPs(d) {
 | 
			
		||||
    var string = [];
 | 
			
		||||
    var ips = d.addresses;
 | 
			
		||||
    ips.sort();
 | 
			
		||||
    ips.forEach(function (ip, i) {
 | 
			
		||||
      if (i > 0) {
 | 
			
		||||
        string.push(V.h('br'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (ip.indexOf('fe80:') !== 0) {
 | 
			
		||||
        string.push(V.h('a', { props: { href: 'http://[' + ip + ']/', target: '_blank' } }, ip));
 | 
			
		||||
      } else {
 | 
			
		||||
        string.push(ip);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return V.h('td', string);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  self.showAutoupdate = function showAutoupdate(d) {
 | 
			
		||||
    return d.autoupdater.enabled ? _.t('node.activated', { branch: d.autoupdater.branch }) : _.t('node.deactivated');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return self;
 | 
			
		||||
});
 | 
			
		||||
@ -3,7 +3,7 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
 | 
			
		||||
  return function (language) {
 | 
			
		||||
    var init = false;
 | 
			
		||||
    var objects = { nodes: {}, links: {} };
 | 
			
		||||
    var objects = {};
 | 
			
		||||
    var targets = [];
 | 
			
		||||
    var views = {};
 | 
			
		||||
    var current = {};
 | 
			
		||||
@ -16,17 +16,20 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function gotoNode(d) {
 | 
			
		||||
      if (d.nodeId in objects.nodes) {
 | 
			
		||||
      if (objects.nodeDict[d.nodeId]) {
 | 
			
		||||
        targets.forEach(function (t) {
 | 
			
		||||
          t.gotoNode(objects.nodes[d.nodeId]);
 | 
			
		||||
          t.gotoNode(objects.nodeDict[d.nodeId], objects.nodeDict);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function gotoLink(d) {
 | 
			
		||||
      if (d.linkId in objects.links) {
 | 
			
		||||
      var link = objects.links.filter(function (value) {
 | 
			
		||||
        return value.id === d.linkId;
 | 
			
		||||
      });
 | 
			
		||||
      if (link) {
 | 
			
		||||
        targets.forEach(function (t) {
 | 
			
		||||
          t.gotoLink(objects.links[d.linkId]);
 | 
			
		||||
          t.gotoLink(link);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -51,7 +54,7 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if (lang && lang !== state.lang && lang === language.getLocale(lang)) {
 | 
			
		||||
        language.setLocale(lang);
 | 
			
		||||
        location.reload();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!init || viewValue && viewValue !== state.view) {
 | 
			
		||||
@ -79,10 +82,10 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var router = new Navigo(null, true);
 | 
			
		||||
    var router = new Navigo(null, true, '#!');
 | 
			
		||||
 | 
			
		||||
    router
 | 
			
		||||
      .on(/^\/?#?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/([\d.]+)\/([\d.]+))?$/, customRoute)
 | 
			
		||||
      .on(/^\/?#?!?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/(-?[\d.]+)\/(-?[\d.]+))?$/, customRoute)
 | 
			
		||||
      .on({
 | 
			
		||||
        '*': function () {
 | 
			
		||||
          router.fullUrl();
 | 
			
		||||
@ -90,7 +93,7 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    router.generateLink = function generateLink(data, full, deep) {
 | 
			
		||||
      var result = '#';
 | 
			
		||||
      var result = '#!';
 | 
			
		||||
 | 
			
		||||
      if (full) {
 | 
			
		||||
        data = Object.assign({}, state, data);
 | 
			
		||||
@ -116,7 +119,7 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    router.getLang = function getLang() {
 | 
			
		||||
      var lang = location.hash.match(/^\/?#\/([\w]{2})\//);
 | 
			
		||||
      var lang = location.hash.match(/^\/?#!?\/([\w]{2})\//);
 | 
			
		||||
      if (lang) {
 | 
			
		||||
        state.lang = language.getLocale(lang[1]);
 | 
			
		||||
        return lang[1];
 | 
			
		||||
@ -139,16 +142,7 @@ define(['Navigo'], function (Navigo) {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    router.setData = function setData(data) {
 | 
			
		||||
      objects.nodes = {};
 | 
			
		||||
      objects.links = {};
 | 
			
		||||
 | 
			
		||||
      data.nodes.all.forEach(function (d) {
 | 
			
		||||
        objects.nodes[d.nodeinfo.node_id] = d;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      data.graph.links.forEach(function (d) {
 | 
			
		||||
        objects.links[d.id] = d;
 | 
			
		||||
      });
 | 
			
		||||
      objects = data;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return router;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								lib/utils/version.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,99 @@
 | 
			
		||||
define(function () {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    reimplate after node-deb-version-compare under MIT
 | 
			
		||||
    (https://github.com/sdumetz/node-deb-version-compare)
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  function Version(v) {
 | 
			
		||||
    var version = /^[a-zA-Z]?([0-9]*(?=:))?:(.*)/.exec(v);
 | 
			
		||||
    this.epoch = (version) ? version[1] : 0;
 | 
			
		||||
    version = (version && version[2]) ? version[2] : v;
 | 
			
		||||
    version = version.split('-');
 | 
			
		||||
    this.debian = (version.length > 1) ? version.pop() : '';
 | 
			
		||||
    this.upstream = version.join('-');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Version.prototype.compare = function (b) {
 | 
			
		||||
    if ((this.epoch > 0 || b.epoch > 0) && Math.sign(this.epoch - b.epoch) !== 0) {
 | 
			
		||||
      return Math.sign(this.epoch - b.epoch);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.compareStrings(this.upstream, b.upstream) !== 0) {
 | 
			
		||||
      return this.compareStrings(this.upstream, b.upstream);
 | 
			
		||||
    }
 | 
			
		||||
    return this.compareStrings(this.debian, b.debian);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Version.prototype.charCode = function (c) { // the lower the charcode the lower the version.
 | 
			
		||||
    // if (c === '~') {return 0;} // tilde sort before anything
 | 
			
		||||
    // else
 | 
			
		||||
    if (/[a-zA-Z]/.test(c)) {
 | 
			
		||||
      return c.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
 | 
			
		||||
    } else if (/[.:+-:]/.test(c)) {
 | 
			
		||||
      return c.charCodeAt(0) + 'z'.charCodeAt(0) + 1;
 | 
			
		||||
    } // charcodes are 46..58
 | 
			
		||||
    return 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // find index of "val" in "ar".
 | 
			
		||||
  Version.prototype.findIndex = function (ar, fn) {
 | 
			
		||||
    for (var i = 0; i < ar.length; i++) {
 | 
			
		||||
      if (fn(ar[i], i)) {
 | 
			
		||||
        return i;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Version.prototype.compareChunk = function (a, b) {
 | 
			
		||||
    var ca = a.split('');
 | 
			
		||||
    var cb = b.split('');
 | 
			
		||||
    var diff = this.findIndex(ca, function (c, index) {
 | 
			
		||||
      return !(cb[index] && c === cb[index]);
 | 
			
		||||
    });
 | 
			
		||||
    if (diff === -1) {
 | 
			
		||||
      if (cb.length > ca.length) {
 | 
			
		||||
        if (cb[ca.length] === '~') {
 | 
			
		||||
          return 1;
 | 
			
		||||
        }
 | 
			
		||||
        return -1;
 | 
			
		||||
      }
 | 
			
		||||
      return 0; // no diff found and same length
 | 
			
		||||
    } else if (!cb[diff]) {
 | 
			
		||||
      return (ca[diff] === '~') ? -1 : 1;
 | 
			
		||||
    }
 | 
			
		||||
    return (this.charCode(ca[diff]) > this.charCode(cb[diff])) ? 1 : -1;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Version.prototype.compareStrings = function (a, b) {
 | 
			
		||||
    if (a === b) {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
    var parseA = /([^0-9]+|[0-9]+)/g;
 | 
			
		||||
    var parseB = /([^0-9]+|[0-9]+)/g;
 | 
			
		||||
    var ra = parseA.exec(a);
 | 
			
		||||
    var rb = parseB.exec(b);
 | 
			
		||||
    while (ra !== null && rb !== null) {
 | 
			
		||||
      if ((isNaN(ra[1]) || isNaN(rb[1])) && ra[1] !== rb[1]) { // a or b is not a number and they're not equal. Note : "" IS a number so both null is impossible
 | 
			
		||||
        return this.compareChunk(ra[1], rb[1]);
 | 
			
		||||
      } // both are numbers
 | 
			
		||||
      if (ra[1] !== rb[1]) {
 | 
			
		||||
        return (parseInt(ra[1], 10) > parseInt(rb[1], 10)) ? 1 : -1;
 | 
			
		||||
      }
 | 
			
		||||
      ra = parseA.exec(a);
 | 
			
		||||
      rb = parseB.exec(b);
 | 
			
		||||
    }
 | 
			
		||||
    if (!ra && rb) { // rb doesn't get exec-ed when ra == null
 | 
			
		||||
      return (rb.length > 0 && rb[1].split('')[0] === '~') ? 1 : -1;
 | 
			
		||||
    } else if (ra && !rb) {
 | 
			
		||||
      return (ra[1].split('')[0] === '~') ? -1 : 1;
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  };
 | 
			
		||||
  return function compare(a, b) {
 | 
			
		||||
    var va = new Version(a[0]);
 | 
			
		||||
    var vb = new Version(b[0]);
 | 
			
		||||
    return vb.compare(va);
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										96
									
								
								locale/cz.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,96 @@
 | 
			
		||||
{
 | 
			
		||||
  "node": {
 | 
			
		||||
    "all": "Všechny uzly",
 | 
			
		||||
    "nodes": "Uzly",
 | 
			
		||||
    "uptime": "Celková doba provozu",
 | 
			
		||||
    "links": "Odkazy",
 | 
			
		||||
    "clients": "Klienti",
 | 
			
		||||
    "distance": "Vzdálenost",
 | 
			
		||||
    "connectionType": "typ připojení",
 | 
			
		||||
    "tq": "tq",
 | 
			
		||||
    "lastOnline": "poslední on-line  %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "lastOffline %{time} (%{date})",
 | 
			
		||||
    "activated": "aktivováno (%{branch})",
 | 
			
		||||
    "deactivated": "deaktivováno",
 | 
			
		||||
    "status": "Stav",
 | 
			
		||||
    "firmware": "Verze firmwaru",
 | 
			
		||||
    "hardware": "Model hardwaru",
 | 
			
		||||
    "visible": "Visible on the map",
 | 
			
		||||
    "update": "Automatický update",
 | 
			
		||||
    "domain": "Domain",
 | 
			
		||||
    "gateway": "Brána",
 | 
			
		||||
    "coordinates": "Souřadnice",
 | 
			
		||||
    "contact": "Kontakt",
 | 
			
		||||
    "primaryMac": "Hlavní MAC",
 | 
			
		||||
    "id": "Identifikace uzlu",
 | 
			
		||||
    "firstSeen": "firstSeen",
 | 
			
		||||
    "systemLoad": "Průměrné zatížení",
 | 
			
		||||
    "ram": "Využití paměti",
 | 
			
		||||
    "ipAddresses": "IP adresa",
 | 
			
		||||
    "nexthop": "Další skok",
 | 
			
		||||
    "selectedGatewayIPv4": "vybranýGatewayIPv4",
 | 
			
		||||
    "selectedGatewayIPv6": "vybranýGatewayIPv6",
 | 
			
		||||
    "link": "Odkaz ||| Odkazy",
 | 
			
		||||
    "node": "Uzel ||| Uzly",
 | 
			
		||||
    "new": "Nové uzly",
 | 
			
		||||
    "missing": "Zmizelé uzly"
 | 
			
		||||
  },
 | 
			
		||||
  "location": {
 | 
			
		||||
    "location": "Poloha",
 | 
			
		||||
    "latitude": "Zeměpisná šířka",
 | 
			
		||||
    "longitude": "Zeměpisná délka",
 | 
			
		||||
    "copy": "Kopírovat"
 | 
			
		||||
  },
 | 
			
		||||
  "sidebar": {
 | 
			
		||||
    "nodeFilter": "nodeFilter",
 | 
			
		||||
    "nodes": "%{total} uzly, %{online} uzly on-line",
 | 
			
		||||
    "clients": "%{smart_count} klienti |||| %{smart_count} klienti",
 | 
			
		||||
    "gateway": " %{smart_count} gateway |||| %{smart_count} gateways",
 | 
			
		||||
    "lastUpdate": "Poslední update",
 | 
			
		||||
    "nodeNew": "nodeNew",
 | 
			
		||||
    "nodeOnline": "Uzel je online",
 | 
			
		||||
    "nodeOffline": "Uzel je offline",
 | 
			
		||||
    "aboutInfo": "aboutInfo",
 | 
			
		||||
    "actual": "aktuální",
 | 
			
		||||
    "stats": "Statistika",
 | 
			
		||||
    "about": "O produktu",
 | 
			
		||||
    "toggle": "přepínat"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Přepnout zobrazení",
 | 
			
		||||
    "location": "Vybrat souřadnice",
 | 
			
		||||
    "tracking": "Lokalizace"
 | 
			
		||||
  },
 | 
			
		||||
  "momentjs": {
 | 
			
		||||
    "calendar": {
 | 
			
		||||
      "sameDay": "[Today at] LT",
 | 
			
		||||
      "nextDay": "[Tomorrow at] LT",
 | 
			
		||||
      "nextWeek": "dddd [at] LT",
 | 
			
		||||
      "lastDay": "[Yesterday at] LT",
 | 
			
		||||
      "lastWeek": "[Last] dddd [at] LT",
 | 
			
		||||
      "sameElse": "L"
 | 
			
		||||
    },
 | 
			
		||||
    "relativeTime": {
 | 
			
		||||
      "future": "in %s",
 | 
			
		||||
      "past": "%s ago",
 | 
			
		||||
      "s": "Několik sekund",
 | 
			
		||||
      "m": "minuta",
 | 
			
		||||
      "mm": "%d minut",
 | 
			
		||||
      "h": "an hour",
 | 
			
		||||
      "hh": "%d hodin",
 | 
			
		||||
      "d": "den",
 | 
			
		||||
      "dd": "%d dnů",
 | 
			
		||||
      "M": "měsíc",
 | 
			
		||||
      "MM": "%d měsíců",
 | 
			
		||||
      "y": "rok",
 | 
			
		||||
      "yy": "%d let"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "yes": "ano",
 | 
			
		||||
  "no": "ne",
 | 
			
		||||
  "unknown": "neznámý",
 | 
			
		||||
  "others": "ostatní",
 | 
			
		||||
  "none": "žádný",
 | 
			
		||||
  "remove": "odstranit",
 | 
			
		||||
  "close": "zavřít"
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
    "links": "Verbindungen",
 | 
			
		||||
    "clients": "Nutzer",
 | 
			
		||||
    "distance": "Entfernung",
 | 
			
		||||
    "connectionType": "Verbindungsart",
 | 
			
		||||
    "tq": "Übertragungsqualität",
 | 
			
		||||
    "lastOnline": "online, letzte Nachricht %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "offline, letzte Nachricht %{time} (%{date})",
 | 
			
		||||
@ -16,17 +17,19 @@
 | 
			
		||||
    "hardware": "Geräte-Modell",
 | 
			
		||||
    "visible": "Auf der Karte sichtbar",
 | 
			
		||||
    "update": "Auto-Update",
 | 
			
		||||
    "site": "Site",
 | 
			
		||||
    "domain": "Domain",
 | 
			
		||||
    "gateway": "Gateway",
 | 
			
		||||
    "coordinates": "Koordinaten",
 | 
			
		||||
    "contact": "Kontakt",
 | 
			
		||||
    "primaryMac": "Primäre MAC",
 | 
			
		||||
    "id": "Knoten ID",
 | 
			
		||||
    "firstSeen": "Erstmals gesehen",
 | 
			
		||||
    "systemLoad": "Load average",
 | 
			
		||||
    "systemLoad": "Systemlast",
 | 
			
		||||
    "ram": "Speicherauslastung",
 | 
			
		||||
    "ipAddresses": "IP Adressen",
 | 
			
		||||
    "selectedGateway": "Gewähltes Gateway",
 | 
			
		||||
    "nexthop": "Nächster Sprung",
 | 
			
		||||
    "selectedGatewayIPv4": "Gewähltes ipv4 Gateway",
 | 
			
		||||
    "selectedGatewayIPv6": "Gewähltes ipv6 Gateway",
 | 
			
		||||
    "link": "Verbindung |||| Verbindungen",
 | 
			
		||||
    "node": "Knoten",
 | 
			
		||||
    "new": "Neue Knoten",
 | 
			
		||||
@ -44,18 +47,20 @@
 | 
			
		||||
    "clients": "mit %{smart_count} Nutzer |||| mit %{smart_count} Nutzern",
 | 
			
		||||
    "gateway": "auf %{smart_count} Gateway |||| auf %{smart_count} Gateways",
 | 
			
		||||
    "lastUpdate": "Letzte Aktualisierung",
 | 
			
		||||
    "nodeNew": "Knoten ist neu",
 | 
			
		||||
    "nodeOnline": "Knoten ist online",
 | 
			
		||||
    "nodeOffline": "Knoten ist offline",
 | 
			
		||||
    "nodeNew": "neu",
 | 
			
		||||
    "nodeOnline": "online",
 | 
			
		||||
    "nodeOffline": "offline",
 | 
			
		||||
    "aboutInfo": "<h2>Über Meshviewer</h2><p>Mit Doppelklick kann man in die Karte hinein zoomen und Shift+Doppelklick heraus zoomen.</p>",
 | 
			
		||||
    "actual": "Aktuell",
 | 
			
		||||
    "stats": "Statistiken",
 | 
			
		||||
    "about": "Über"
 | 
			
		||||
    "about": "Über",
 | 
			
		||||
    "toggle": "Seitenleiste anzeigen/ausblenden"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Ansicht wechseln",
 | 
			
		||||
    "location": "Koordinaten wählen",
 | 
			
		||||
    "tracking": "Lokalisierung"
 | 
			
		||||
    "tracking": "Lokalisierung",
 | 
			
		||||
    "fullscreen": "Vollbildmodus wechseln"
 | 
			
		||||
  },
 | 
			
		||||
  "momentjs": {
 | 
			
		||||
    "calendar": {
 | 
			
		||||
@ -85,5 +90,8 @@
 | 
			
		||||
  "yes": "ja",
 | 
			
		||||
  "no": "nein",
 | 
			
		||||
  "unknown": "unbekannt",
 | 
			
		||||
  "none": "keine"
 | 
			
		||||
  "others": "andere",
 | 
			
		||||
  "none": "keine",
 | 
			
		||||
  "remove": "entfernen",
 | 
			
		||||
  "close": "schließen"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
    "links": "Links",
 | 
			
		||||
    "clients": "Clients",
 | 
			
		||||
    "distance": "Distance",
 | 
			
		||||
    "connectionType": "Connection type",
 | 
			
		||||
    "tq": "Transmit quality",
 | 
			
		||||
    "lastOnline": "online, last message %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "offline, last message %{time} (%{date})",
 | 
			
		||||
@ -16,7 +17,7 @@
 | 
			
		||||
    "hardware": "Hardware model",
 | 
			
		||||
    "visible": "Visible on the map",
 | 
			
		||||
    "update": "Auto update",
 | 
			
		||||
    "site": "Site",
 | 
			
		||||
    "domain": "Domain",
 | 
			
		||||
    "gateway": "Gateway",
 | 
			
		||||
    "coordinates": "Coordinates",
 | 
			
		||||
    "contact": "Contact",
 | 
			
		||||
@ -26,7 +27,9 @@
 | 
			
		||||
    "systemLoad": "Load average",
 | 
			
		||||
    "ram": "Memory usage",
 | 
			
		||||
    "ipAddresses": "IP addresses",
 | 
			
		||||
    "selectedGateway": "Selected gateway",
 | 
			
		||||
    "nexthop": "Nexthop",
 | 
			
		||||
    "selectedGatewayIPv4": "Selected ipv4-gateway",
 | 
			
		||||
    "selectedGatewayIPv6": "Selected ipv6-gateway",
 | 
			
		||||
    "link": "Link |||| Links",
 | 
			
		||||
    "node": "Node |||| Nodes",
 | 
			
		||||
    "new": "New nodes",
 | 
			
		||||
@ -44,18 +47,20 @@
 | 
			
		||||
    "clients": "with %{smart_count} client |||| with %{smart_count} clients",
 | 
			
		||||
    "gateway": "on %{smart_count} gateway |||| on %{smart_count} gateways",
 | 
			
		||||
    "lastUpdate": "Last update",
 | 
			
		||||
    "nodeNew": "Node is new",
 | 
			
		||||
    "nodeOnline": "Node is online",
 | 
			
		||||
    "nodeOffline": "Node is offline",
 | 
			
		||||
    "nodeNew": "new",
 | 
			
		||||
    "nodeOnline": "online",
 | 
			
		||||
    "nodeOffline": "offline",
 | 
			
		||||
    "aboutInfo": "<h2>About Meshviewer</h2> <p>You can zoom in with double-click and zoom out with shift+double-click</p>",
 | 
			
		||||
    "actual": "Current",
 | 
			
		||||
    "stats": "Statistics",
 | 
			
		||||
    "about": "About"
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "toggle": "Toggle Sidebar"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Switch view",
 | 
			
		||||
    "location": "Pick coordinates",
 | 
			
		||||
    "tracking": "Localisation"
 | 
			
		||||
    "tracking": "Localisation",
 | 
			
		||||
    "fullscreen": "Toggle fullscreen"
 | 
			
		||||
  },
 | 
			
		||||
  "momentjs": {
 | 
			
		||||
    "calendar": {
 | 
			
		||||
@ -85,5 +90,8 @@
 | 
			
		||||
  "yes": "yes",
 | 
			
		||||
  "no": "no",
 | 
			
		||||
  "unknown": "unknown",
 | 
			
		||||
  "none": "none"
 | 
			
		||||
  "others": "other",
 | 
			
		||||
  "none": "none",
 | 
			
		||||
  "remove": "remove",
 | 
			
		||||
  "close": "close"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
    "links": "Connexion",
 | 
			
		||||
    "clients": "Clients",
 | 
			
		||||
    "distance": "Distance",
 | 
			
		||||
    "connectionType": "Type de connexion",
 | 
			
		||||
    "tq": "Qualité de transmission",
 | 
			
		||||
    "lastOnline": "en ligne, dernier message %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "hors ligne, dernier message %{time} (%{date})",
 | 
			
		||||
@ -16,7 +17,7 @@
 | 
			
		||||
    "hardware": "Modèle matériel",
 | 
			
		||||
    "visible": "Visible sur la carte",
 | 
			
		||||
    "update": "Mise à jour automatique",
 | 
			
		||||
    "site": "Site",
 | 
			
		||||
    "domain": "Domain",
 | 
			
		||||
    "gateway": "Passerelle",
 | 
			
		||||
    "coordinates": "Coordonnées",
 | 
			
		||||
    "contact": "Contact",
 | 
			
		||||
@ -26,7 +27,9 @@
 | 
			
		||||
    "systemLoad": "Charge moyenne",
 | 
			
		||||
    "ram": "Utilisation de la mémoire",
 | 
			
		||||
    "ipAddresses": "Adresse IP",
 | 
			
		||||
    "selectedGateway": "Passerelle sélectionné",
 | 
			
		||||
    "nexthop": "Nexthop",
 | 
			
		||||
    "selectedGatewayIPv4": "Selected ipv4-gateway",
 | 
			
		||||
    "selectedGatewayIPv6": "Selected ipv6-gateway",
 | 
			
		||||
    "link": "Connexion |||| Connexions",
 | 
			
		||||
    "node": "Nœud |||| Nœuds",
 | 
			
		||||
    "new": "Nouveaux nœuds",
 | 
			
		||||
@ -50,7 +53,8 @@
 | 
			
		||||
    "aboutInfo": "<h2>Sur Meshviewer</h2> <p>Vous pouvez zoomer avec double-clic et effectuer un zoom arrière avec shift + double-clic</p>",
 | 
			
		||||
    "actual": "Actuel",
 | 
			
		||||
    "stats": "Statistiques",
 | 
			
		||||
    "about": "À propros"
 | 
			
		||||
    "about": "À propros",
 | 
			
		||||
    "toggle": "Toggle Sidebar"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Basculer l’affichage",
 | 
			
		||||
@ -85,5 +89,8 @@
 | 
			
		||||
  "yes": "oui",
 | 
			
		||||
  "no": "non",
 | 
			
		||||
  "unknown": "inconnu",
 | 
			
		||||
  "none": "aucun"
 | 
			
		||||
  "others": "autres",
 | 
			
		||||
  "none": "aucun",
 | 
			
		||||
  "remove": "supprimer",
 | 
			
		||||
  "close": "fermer"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
    "links": "Ссылки",
 | 
			
		||||
    "clients": "Клиенты",
 | 
			
		||||
    "distance": "Расстояние",
 | 
			
		||||
    "connectionType": "Тип подключения",
 | 
			
		||||
    "tq": "Качество связи",
 | 
			
		||||
    "lastOnline": "в сети, последнее сообщение %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "не в сети, последнее сообщение %{time} (%{date})",
 | 
			
		||||
@ -16,7 +17,7 @@
 | 
			
		||||
    "hardware": "Тип оборудования",
 | 
			
		||||
    "visible": "Видно на карте",
 | 
			
		||||
    "update": "Автообновление",
 | 
			
		||||
    "site": "Сайт",
 | 
			
		||||
    "domain": "Сайт",
 | 
			
		||||
    "gateway": "Шлюз",
 | 
			
		||||
    "coordinates": "Координаты",
 | 
			
		||||
    "contact": "Контакты",
 | 
			
		||||
@ -26,7 +27,9 @@
 | 
			
		||||
    "systemLoad": "Средняя загрузка",
 | 
			
		||||
    "ram": "Используемая память",
 | 
			
		||||
    "ipAddresses": "IP адреса",
 | 
			
		||||
    "selectedGateway": "Выбранный шлюз",
 | 
			
		||||
    "nexthop": "Следующий скачок",
 | 
			
		||||
    "selectedGatewayIPv4": "Выбранный шлюз ipv4",
 | 
			
		||||
    "selectedGatewayIPv6": "Выбранный шлюз ipv6",
 | 
			
		||||
    "link": "Ссылка |||| Ссылки",
 | 
			
		||||
    "node": "Узел |||| Узлы",
 | 
			
		||||
    "new": "Новые узлы",
 | 
			
		||||
@ -50,7 +53,8 @@
 | 
			
		||||
    "aboutInfo": "<h2>О Meshviewer</h2> <p>Вы можете увеличить масштаб двойным щелчком мыши и уменьшить с shift + двойной щелчок</p>",
 | 
			
		||||
    "actual": "Текущее",
 | 
			
		||||
    "stats": "Статистика",
 | 
			
		||||
    "about": "О продукте"
 | 
			
		||||
    "about": "О продукте",
 | 
			
		||||
    "toggle": "Включить панель"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Переключить вид",
 | 
			
		||||
@ -85,5 +89,8 @@
 | 
			
		||||
  "yes": "да",
 | 
			
		||||
  "no": "нет",
 | 
			
		||||
  "unknown": "неизвестно",
 | 
			
		||||
  "none": "нет"
 | 
			
		||||
  "others": "другие",
 | 
			
		||||
  "none": "нет",
 | 
			
		||||
  "remove": "убрать",
 | 
			
		||||
  "close": "закрыть"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										96
									
								
								locale/tr.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,96 @@
 | 
			
		||||
{
 | 
			
		||||
  "node": {
 | 
			
		||||
    "all": "Bütün düğümler",
 | 
			
		||||
    "nodes": "Düğümler",
 | 
			
		||||
    "uptime": "Çalışma süresi",
 | 
			
		||||
    "links": "Bağlantılar",
 | 
			
		||||
    "clients": "Müşteriler",
 | 
			
		||||
    "distance": "Mesafe",
 | 
			
		||||
    "connectionType": "Bağlantı türü",
 | 
			
		||||
    "tq": "İletim kalitesi",
 | 
			
		||||
    "lastOnline": "çevrimiçi, son mesaj %{time} (%{date})",
 | 
			
		||||
    "lastOffline": "çevrimdışı, son mesaj %{time} (%{date})",
 | 
			
		||||
    "activated": "aktif (%{branch})",
 | 
			
		||||
    "deactivated": "devredışı bırakıldı",
 | 
			
		||||
    "status": "Durum",
 | 
			
		||||
    "firmware": "Yazılım versiyonu",
 | 
			
		||||
    "hardware": "Donanım modeli",
 | 
			
		||||
    "visible": "Harita üzerinde görünür",
 | 
			
		||||
    "update": "Otomatik güncelleme",
 | 
			
		||||
    "domain": "Domain",
 | 
			
		||||
    "gateway": "Geçit",
 | 
			
		||||
    "coordinates": "Koordinatlar",
 | 
			
		||||
    "contact": "İlişki",
 | 
			
		||||
    "primaryMac": "Birincil MAC",
 | 
			
		||||
    "id": "Düğüm kimliği",
 | 
			
		||||
    "firstSeen": "İlk görülme",
 | 
			
		||||
    "systemLoad": "Ortalama yük",
 | 
			
		||||
    "ram": "Bellek kullanımı",
 | 
			
		||||
    "ipAddresses": "IP adresleri",
 | 
			
		||||
    "nexthop": "Bir sonraki atlama",
 | 
			
		||||
    "selectedGatewayIPv4": "Seçili Ipv4-ağ geçidi",
 | 
			
		||||
    "selectedGatewayIPv6": "Seçili Ipv6-ağ geçidi",
 | 
			
		||||
    "link": "Bağlantı ||| Bağlantılar",
 | 
			
		||||
    "node": "Düğüm ||| Düğümler",
 | 
			
		||||
    "new": "Yeni düğümler",
 | 
			
		||||
    "missing": "Kaybolan düğümler"
 | 
			
		||||
  },
 | 
			
		||||
  "location": {
 | 
			
		||||
    "location": "Konum",
 | 
			
		||||
    "latitude": "Enlem",
 | 
			
		||||
    "longitude": "Boylam",
 | 
			
		||||
    "copy": "Kopya"
 | 
			
		||||
  },
 | 
			
		||||
  "sidebar": {
 | 
			
		||||
    "nodeFilter": "Düğüm Filtresi",
 | 
			
		||||
    "nodes": "%{total} düğümler, %{online} çevrimiçi düğümler dahil",
 | 
			
		||||
    "clients": "%{smart_count} müşteri ile |||| %{smart_count} müşteriler ile",
 | 
			
		||||
    "gateway": "%{smart_count} geçit üzerinde |||| %{smart_count} geçitler üzerinde",
 | 
			
		||||
    "lastUpdate": "Son güncelleme",
 | 
			
		||||
    "nodeNew": "yeni",
 | 
			
		||||
    "nodeOnline": "çevrimiçi",
 | 
			
		||||
    "nodeOffline": "çevrimdışı",
 | 
			
		||||
    "aboutInfo": "<h2>Meshviewer Hakkında</h2> <p>Çift tıklayarak yakınlaştırabilir ve Shift tuşuna basıp+çift tıklayarak uzaklaştırabilirsiniz</p>",
 | 
			
		||||
    "actual": "Mevcut",
 | 
			
		||||
    "stats": "İstatistikler",
 | 
			
		||||
    "about": "Hakkında",
 | 
			
		||||
    "toggle": "Kenar çubuğunu değiştir"
 | 
			
		||||
  },
 | 
			
		||||
  "button": {
 | 
			
		||||
    "switchView": "Görünümü Değiştir",
 | 
			
		||||
    "location": "Koordinatları seç",
 | 
			
		||||
    "tracking": "Yerelleştirme"
 | 
			
		||||
  },
 | 
			
		||||
  "momentjs": {
 | 
			
		||||
    "calendar": {
 | 
			
		||||
      "sameDay": "[Bugün] LT",
 | 
			
		||||
      "nextDay": "[Yarın] LT",
 | 
			
		||||
      "nextWeek": "dddd [at] LT",
 | 
			
		||||
      "lastDay": "[Dün] LT",
 | 
			
		||||
      "lastWeek": "[Last] dddd [at] LT",
 | 
			
		||||
      "sameElse": "L"
 | 
			
		||||
    },
 | 
			
		||||
    "relativeTime": {
 | 
			
		||||
      "future": "%s içinde",
 | 
			
		||||
      "past": "%s önce",
 | 
			
		||||
      "s": "birkaç saniye",
 | 
			
		||||
      "m": "bir dakika",
 | 
			
		||||
      "mm": "%d dakikalar",
 | 
			
		||||
      "h": "bir saat",
 | 
			
		||||
      "hh": "%d saatler",
 | 
			
		||||
      "d": "bir gün",
 | 
			
		||||
      "dd": "%d günler",
 | 
			
		||||
      "M": "bir ay",
 | 
			
		||||
      "MM": "%d aylar",
 | 
			
		||||
      "y": "bir yıl",
 | 
			
		||||
      "yy": "%d yıllar"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "yes": "evet",
 | 
			
		||||
  "no": "hayır",
 | 
			
		||||
  "unknown": "bilinmeyen",
 | 
			
		||||
  "others": "diğer",
 | 
			
		||||
  "none": "hiçbiri",
 | 
			
		||||
  "remove": "kaldır",
 | 
			
		||||
  "close": "kapat"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ffrgb-meshviewer",
 | 
			
		||||
  "name": "meshviewer",
 | 
			
		||||
  "version": "11.1.0",
 | 
			
		||||
  "license": "AGPL-3.0",
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
@ -9,28 +10,30 @@
 | 
			
		||||
    "url": "https://github.com/ffrgb/meshviewer/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "babel-eslint": "^7.2.3",
 | 
			
		||||
    "browser-sync": "^2.18.8",
 | 
			
		||||
    "eslint": "^3.19.0",
 | 
			
		||||
    "eslint-config-airbnb-es5": "^1.1.0",
 | 
			
		||||
    "babel-eslint": "^10.0.1",
 | 
			
		||||
    "browser-sync": "^2.26.5",
 | 
			
		||||
    "del": "^5.1.0",
 | 
			
		||||
    "eslint": "^6.5.1",
 | 
			
		||||
    "eslint-config-airbnb-es5": "^1.2.0",
 | 
			
		||||
    "eslint-config-defaults": "^9.0.0",
 | 
			
		||||
    "eslint-plugin-react": "^7.0.0",
 | 
			
		||||
    "gulp": "github:gulpjs/gulp#4.0",
 | 
			
		||||
    "gulp-autoprefixer": "^3.1.1",
 | 
			
		||||
    "gulp-cache-bust": "^1.1.0",
 | 
			
		||||
    "eslint-plugin-react": "^7.12.4",
 | 
			
		||||
    "gulp": "^4.0.1",
 | 
			
		||||
    "gulp-autoprefixer": "^7.0.1",
 | 
			
		||||
    "gulp-cache-bust": "^1.4.0",
 | 
			
		||||
    "gulp-cli": "^2.2.0",
 | 
			
		||||
    "gulp-environments": "^0.1.2",
 | 
			
		||||
    "gulp-eslint": "^3.0.1",
 | 
			
		||||
    "gulp-htmlmin": "^3.0.0",
 | 
			
		||||
    "gulp-inject": "^4.2.0",
 | 
			
		||||
    "gulp-jsonminify": "^1.0.0",
 | 
			
		||||
    "gulp-kyh-inline-source": "^3.0.2",
 | 
			
		||||
    "gulp-load-plugins": "^1.5.0",
 | 
			
		||||
    "gulp-real-favicon": "^0.2.2",
 | 
			
		||||
    "gulp-requirejs-optimize": "^1.2.0",
 | 
			
		||||
    "gulp-sass": "^3.1.0",
 | 
			
		||||
    "gulp-sass-lint": "^1.3.2",
 | 
			
		||||
    "gulp-sourcemaps": "^2.6.0",
 | 
			
		||||
    "gulp-uglify": "^2.1.2"
 | 
			
		||||
    "gulp-eslint": "^6.0.0",
 | 
			
		||||
    "gulp-htmlmin": "^5.0.1",
 | 
			
		||||
    "gulp-inject": "^5.0.2",
 | 
			
		||||
    "gulp-inline-source": "^4.0.0",
 | 
			
		||||
    "gulp-jsonminify": "^1.1.0",
 | 
			
		||||
    "gulp-load-plugins": "^2.0.1",
 | 
			
		||||
    "gulp-real-favicon": "^0.3.2",
 | 
			
		||||
    "gulp-requirejs-optimize": "^1.3.0",
 | 
			
		||||
    "gulp-sass": "^4.0.2",
 | 
			
		||||
    "gulp-sass-lint": "^1.4.0",
 | 
			
		||||
    "gulp-sourcemaps": "^2.6.5",
 | 
			
		||||
    "gulp-uglify": "^3.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "env": {
 | 
			
		||||
@ -42,17 +45,23 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "almond": "^0.3.3",
 | 
			
		||||
    "d3-drag": "^1.0.4",
 | 
			
		||||
    "d3-force": "^1.0.6",
 | 
			
		||||
    "d3-selection": "^1.0.6",
 | 
			
		||||
    "d3-zoom": "^1.1.3",
 | 
			
		||||
    "leaflet": "^1.0.3",
 | 
			
		||||
    "moment": "^2.17.1",
 | 
			
		||||
    "navigo": "^4.7.1",
 | 
			
		||||
    "node-polyglot": "^2.2.2",
 | 
			
		||||
    "promise-polyfill": "^6.0.2",
 | 
			
		||||
    "rbush": "^2.0.1",
 | 
			
		||||
    "requirejs": "^2.3.2",
 | 
			
		||||
    "snabbdom": "^0.6.4"
 | 
			
		||||
  }
 | 
			
		||||
    "d3-drag": "^1.2.4",
 | 
			
		||||
    "d3-force": "^1.2.1",
 | 
			
		||||
    "d3-selection": "^1.4.0",
 | 
			
		||||
    "d3-zoom": "^1.8.3",
 | 
			
		||||
    "leaflet": "^1.5.1",
 | 
			
		||||
    "moment": "^2.24.0",
 | 
			
		||||
    "navigo": "^7.1.2",
 | 
			
		||||
    "node-polyglot": "2.2.2",
 | 
			
		||||
    "promise-polyfill": "^8.1.3",
 | 
			
		||||
    "rbush": "^3.0.1",
 | 
			
		||||
    "requirejs": "^2.3.6",
 | 
			
		||||
    "snabbdom": "^0.7.3"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "gulp": "./node_modules/gulp-cli/bin/gulp.js"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "> 1% in DE"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										72
									
								
								polyfill.js
									
									
									
									
									
								
							
							
						
						@ -7,56 +7,8 @@ if (!String.prototype.includes) {
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!String.prototype.startsWith) {
 | 
			
		||||
  String.prototype.startsWith = function (searchString, position) {
 | 
			
		||||
    position = position || 0;
 | 
			
		||||
    return this.substr(position, searchString.length) === searchString;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!String.prototype.repeat) {
 | 
			
		||||
  String.prototype.repeat = function (count) {
 | 
			
		||||
    'use strict';
 | 
			
		||||
    if (this === null) {
 | 
			
		||||
      throw new TypeError('can\'t convert ' + this + ' to object');
 | 
			
		||||
    }
 | 
			
		||||
    var str = '' + this;
 | 
			
		||||
    count = +count;
 | 
			
		||||
    if (count < 0) {
 | 
			
		||||
      throw new RangeError('repeat count must be non-negative');
 | 
			
		||||
    }
 | 
			
		||||
    if (count === Infinity) {
 | 
			
		||||
      throw new RangeError('repeat count must be less than infinity');
 | 
			
		||||
    }
 | 
			
		||||
    count = Math.floor(count);
 | 
			
		||||
    if (str.length === 0 || count === 0) {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
    // Ensuring count is a 31-bit integer allows us to heavily optimize the
 | 
			
		||||
    // main part. But anyway, most current (August 2014) browsers can't handle
 | 
			
		||||
    // strings 1 << 28 chars or longer, so:
 | 
			
		||||
    if (str.length * count >= 1 << 28) {
 | 
			
		||||
      throw new RangeError('repeat count must not overflow maximum string size');
 | 
			
		||||
    }
 | 
			
		||||
    var rpt = '';
 | 
			
		||||
    for (; ;) {
 | 
			
		||||
      if ((count & 1) === 1) {
 | 
			
		||||
        rpt += str;
 | 
			
		||||
      }
 | 
			
		||||
      count >>>= 1;
 | 
			
		||||
      if (count === 0) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      str += str;
 | 
			
		||||
    }
 | 
			
		||||
    // Could we try:
 | 
			
		||||
    // return Array(count + 1).join(this);
 | 
			
		||||
    return rpt;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (typeof Object.assign !== 'function') {
 | 
			
		||||
  Object.assign = function(target, varArgs) { // .length of function is 2
 | 
			
		||||
  Object.assign = function (target, varArgs) { // .length of function is 2
 | 
			
		||||
    if (target == null) { // TypeError if undefined or null
 | 
			
		||||
      throw new TypeError('Cannot convert undefined or null to object');
 | 
			
		||||
    }
 | 
			
		||||
@ -78,3 +30,25 @@ if (typeof Object.assign !== 'function') {
 | 
			
		||||
    return to;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line consistent-return
 | 
			
		||||
(function () {
 | 
			
		||||
  if (typeof window.CustomEvent === 'function') {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function CustomEvent(event, params) {
 | 
			
		||||
    params = params || { bubbles: false, cancelable: false, detail: undefined };
 | 
			
		||||
    var evt = document.createEvent('CustomEvent');
 | 
			
		||||
    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
 | 
			
		||||
    return evt;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  CustomEvent.prototype = window.Event.prototype;
 | 
			
		||||
 | 
			
		||||
  window.CustomEvent = CustomEvent;
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
if ('serviceWorker' in navigator) {
 | 
			
		||||
  navigator.serviceWorker.register('service-worker.js');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@
 | 
			
		||||
@import 'custom/variables';
 | 
			
		||||
 | 
			
		||||
// Mixins
 | 
			
		||||
@import 'mixins/shadow';
 | 
			
		||||
@import 'mixins/icon';
 | 
			
		||||
@import 'mixins/font';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
// Original is in LESS and can be found here: https://gist.github.com/gefangenimnetz/3ef3e18364edf105c5af
 | 
			
		||||
@mixin shadow($level: 1) {
 | 
			
		||||
  @if $level == 1 {
 | 
			
		||||
    box-shadow: 0 1px 3px transparentize($color-black, .88), 0 1px 2px transparentize($color-black, .76);
 | 
			
		||||
  } @else if $level == 2 {
 | 
			
		||||
    box-shadow: 0 3px 6px transparentize($color-black, .84), 0 3px 6px transparentize($color-black, .77);
 | 
			
		||||
  } @else if $level == 3 {
 | 
			
		||||
    box-shadow: 0 10px 20px transparentize($color-black, .81), 0 6px 6px transparentize($color-black, .77);
 | 
			
		||||
  } @else if $level == 4 {
 | 
			
		||||
    box-shadow: 0 14px 28px transparentize($color-black, .75), 0 10px 10px transparentize($color-black, .78);
 | 
			
		||||
  } @else if $level == 5 {
 | 
			
		||||
    box-shadow: 0 19px 38px transparentize($color-black, .7), 0 15px 12px transparentize($color-black, .78);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,6 +13,12 @@ header {
 | 
			
		||||
  border-bottom: 1px solid darken($color-white, 10%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea,
 | 
			
		||||
input {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  color: $color-black, 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
@ -22,11 +28,7 @@ h6 {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1 {
 | 
			
		||||
  font-size: 2em;
 | 
			
		||||
  padding: .67em 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2 {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  padding: .83em 0;
 | 
			
		||||
@ -37,6 +39,7 @@ h3 {
 | 
			
		||||
  padding: 1em 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3 {
 | 
			
		||||
  padding-left: $button-distance;
 | 
			
		||||
@ -57,6 +60,10 @@ img {
 | 
			
		||||
a {
 | 
			
		||||
  color: $color-online;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
 | 
			
		||||
  &:focus {
 | 
			
		||||
    color: darken($color-online, 15%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
@ -70,3 +77,15 @@ strong {
 | 
			
		||||
.hide {
 | 
			
		||||
  display: none !important; // sass-lint:disable-line no-important
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sr-only {
 | 
			
		||||
  border: 0;
 | 
			
		||||
  clip: rect(0, 0, 0, 0);
 | 
			
		||||
  clip-path: inset(50%);
 | 
			
		||||
  height: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  width: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,8 @@ button {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.active {
 | 
			
		||||
  &.active,
 | 
			
		||||
  &:focus {
 | 
			
		||||
    box-shadow: 0 0 0 2px $color-primary;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -37,20 +38,6 @@ button {
 | 
			
		||||
    color: $color-primary;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @if $shadows == 1 {
 | 
			
		||||
    &.shadow {
 | 
			
		||||
      @include shadow(1);
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        @include shadow(2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:active {
 | 
			
		||||
        box-shadow: inset 0 5px 20px transparentize($color-black, .81), inset 0 3px 6px transparentize($color-black, .77);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Tooltip
 | 
			
		||||
  &[data-tooltip] {
 | 
			
		||||
    &::after {
 | 
			
		||||
@ -78,9 +65,6 @@ button {
 | 
			
		||||
  &.close {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    @if $shadows == 1 {
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
    }
 | 
			
		||||
    color: transparentize($color-black, .5);
 | 
			
		||||
    float: right;
 | 
			
		||||
    font-size: $button-font-size;
 | 
			
		||||
@ -90,3 +74,33 @@ button {
 | 
			
		||||
    width: auto;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tooltip
 | 
			
		||||
// sass-lint:disable-block nesting-depth
 | 
			
		||||
.content,
 | 
			
		||||
.sidebar > {
 | 
			
		||||
  button {
 | 
			
		||||
    &[aria-label] {
 | 
			
		||||
      &::after {
 | 
			
		||||
        background: $color-black;
 | 
			
		||||
        border-radius: 3px;
 | 
			
		||||
        color: $color-white;
 | 
			
		||||
        content: attr(aria-label);
 | 
			
		||||
        font-family: $font-family;
 | 
			
		||||
        font-size: $font-size;
 | 
			
		||||
        padding: 0 12px;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        transform: translate(45px, 52px);
 | 
			
		||||
        visibility: hidden;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        &::after {
 | 
			
		||||
          transition: visibility 0s linear .3s;
 | 
			
		||||
          visibility: visible;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,6 +57,10 @@
 | 
			
		||||
      outline: none;
 | 
			
		||||
      padding: 0 2px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
 | 
			
		||||
      &:focus {
 | 
			
		||||
        background: transparentize($color-primary, .95);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    button {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,30 @@
 | 
			
		||||
.infobox {
 | 
			
		||||
  .clients {
 | 
			
		||||
    color: $color-online;
 | 
			
		||||
    font-family: $font-family-icons;
 | 
			
		||||
  .clients,
 | 
			
		||||
  .gateway {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: wrap;
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      flex-grow: 1;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ion-people,
 | 
			
		||||
    .ion-arrow-right-c {
 | 
			
		||||
      font-size: 1.5em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .node-links {
 | 
			
		||||
    table-layout: fixed;
 | 
			
		||||
 | 
			
		||||
    th,
 | 
			
		||||
    td {
 | 
			
		||||
      &:nth-child(3),
 | 
			
		||||
      &:nth-child(5) {
 | 
			
		||||
        width: 12%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  input,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
header {
 | 
			
		||||
  h2 {
 | 
			
		||||
  h1 {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -17,13 +17,23 @@ header {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.legend {
 | 
			
		||||
  .symbol {
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    height: 1em;
 | 
			
		||||
    vertical-align: -5%;
 | 
			
		||||
    width: 1em;
 | 
			
		||||
  a {
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  span {
 | 
			
		||||
    &:not(:first-child) {
 | 
			
		||||
      margin-left: 1em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.symbol {
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  height: 1em;
 | 
			
		||||
  vertical-align: -5%;
 | 
			
		||||
  width: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dot looks compared to thin font a bit darker - lighten it 10%
 | 
			
		||||
@ -45,7 +55,20 @@ header {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.legend-online,
 | 
			
		||||
.legend-offline {
 | 
			
		||||
  margin-left: 1em;
 | 
			
		||||
.legend-24ghz {
 | 
			
		||||
  .symbol {
 | 
			
		||||
    background-color: $color-24ghz;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.legend-5ghz {
 | 
			
		||||
  .symbol {
 | 
			
		||||
    background-color: $color-5ghz;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.legend-others {
 | 
			
		||||
  .symbol {
 | 
			
		||||
    background-color: $color-others;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,10 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @media screen and (max-width: map-get($grid-breakpoints, lg) - 1) {
 | 
			
		||||
      right: -1rem;
 | 
			
		||||
      right: .1rem;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      transform: scale(.8);
 | 
			
		||||
      transform-origin: right;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
    background: $color-new;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    height: 1.4em;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  label {
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,14 @@
 | 
			
		||||
 | 
			
		||||
  span {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    color: $color-white;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    min-width: 1.5em;
 | 
			
		||||
    padding: .25em .5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,13 @@
 | 
			
		||||
    .sidebarhandle {
 | 
			
		||||
      left: $button-distance;
 | 
			
		||||
      transform: scale(-1, 1);
 | 
			
		||||
 | 
			
		||||
      // sass-lint:disable-block nesting-depth
 | 
			
		||||
      &[aria-label] {
 | 
			
		||||
        &::after {
 | 
			
		||||
          transform: scale(-1, 1) translate(105px, 52px) !important; // sass-lint:disable-line no-important
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    @media screen and (max-width: map-get($grid-breakpoints, lg) - 1) {
 | 
			
		||||
      width: auto;
 | 
			
		||||
@ -23,7 +30,8 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .node-list,
 | 
			
		||||
  .node-links {
 | 
			
		||||
  .node-links,
 | 
			
		||||
  .link-list {
 | 
			
		||||
    th,
 | 
			
		||||
    td {
 | 
			
		||||
      &:first-child {
 | 
			
		||||
@ -40,36 +48,29 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .link-list {
 | 
			
		||||
    th,
 | 
			
		||||
    td {
 | 
			
		||||
      &:nth-child(2) {
 | 
			
		||||
        width: 60%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .node-links {
 | 
			
		||||
    padding-bottom: 15px;
 | 
			
		||||
 | 
			
		||||
    th,
 | 
			
		||||
    td {
 | 
			
		||||
      &:first-child {
 | 
			
		||||
        width: 50px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .link-list {
 | 
			
		||||
    th,
 | 
			
		||||
    td {
 | 
			
		||||
      &:nth-child(1) {
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        text-overflow: ellipsis;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        width: 66%;
 | 
			
		||||
        width: 35px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .container {
 | 
			
		||||
    @if $shadows == 1 {
 | 
			
		||||
      @include shadow(2);
 | 
			
		||||
    } @else {
 | 
			
		||||
      border-right: 1px solid darken($color-white, 10%);
 | 
			
		||||
    }
 | 
			
		||||
    background: transparentize($color-white, .03);
 | 
			
		||||
    border-right: 1px solid darken($color-white, 10%);
 | 
			
		||||
    min-height: 100vh;
 | 
			
		||||
    overflow-y: visible;
 | 
			
		||||
 | 
			
		||||
@ -91,9 +92,6 @@
 | 
			
		||||
    .container,
 | 
			
		||||
    .infobox {
 | 
			
		||||
      border-radius: 0;
 | 
			
		||||
      @if $shadows == 1 {
 | 
			
		||||
        box-shadow: none;
 | 
			
		||||
      }
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -120,13 +118,19 @@
 | 
			
		||||
  left: $sidebar-width + 2 * $button-distance;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: $button-distance;
 | 
			
		||||
  transition: left .5s, box-shadow .5s, color .5s, transform .5s;
 | 
			
		||||
  transition: left .5s, color .5s, transform .5s;
 | 
			
		||||
  z-index: 1010;
 | 
			
		||||
 | 
			
		||||
  &::after {
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: '\f124';
 | 
			
		||||
    padding-right: .125em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[aria-label] {
 | 
			
		||||
    &::after {
 | 
			
		||||
      transform: translate(-45px, 52px) !important; // sass-lint:disable-line no-important
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.online {
 | 
			
		||||
@ -136,7 +140,3 @@
 | 
			
		||||
.offline {
 | 
			
		||||
  color: $color-offline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.unseen {
 | 
			
		||||
  color: $color-unseen;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,16 @@ table {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr {
 | 
			
		||||
  &.header {
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
 | 
			
		||||
    th {
 | 
			
		||||
      padding-top: 1em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td,
 | 
			
		||||
th {
 | 
			
		||||
  line-height: 1.41em;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,7 @@
 | 
			
		||||
.tabs {
 | 
			
		||||
  @if $shadows == 1 {
 | 
			
		||||
    @include shadow(1);
 | 
			
		||||
  } @else {
 | 
			
		||||
    border: 0 solid darken($color-white, 10%);
 | 
			
		||||
    border-bottom-width: 1px;
 | 
			
		||||
  }
 | 
			
		||||
  background: transparentize($color-black, .98);
 | 
			
		||||
  border: 0 solid darken($color-white, 10%);
 | 
			
		||||
  border-bottom-width: 1px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  display: -webkit-flex; // sass-lint:disable-line no-vendor-prefixes no-duplicate-properties
 | 
			
		||||
  list-style: none;
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,10 @@ $color-primary: #dc0067 !default;
 | 
			
		||||
$color-new: #459c18 !default;
 | 
			
		||||
$color-online: #1566a9 !default;
 | 
			
		||||
$color-offline: #cf3e2a !default;
 | 
			
		||||
$color-unseen: #d89100 !default;
 | 
			
		||||
 | 
			
		||||
$color-24ghz: $color-primary !default;
 | 
			
		||||
$color-5ghz: #e3a619 !default;
 | 
			
		||||
$color-others: #0a9c92 !default;
 | 
			
		||||
 | 
			
		||||
$color-map-background: #f8f4f0 !default;
 | 
			
		||||
 | 
			
		||||
@ -43,8 +46,5 @@ $grid-breakpoints: (
 | 
			
		||||
$sidebar-width: map-get($grid-breakpoints, xl) * .45 !default;
 | 
			
		||||
$sidebar-width-small: map-get($grid-breakpoints, lg) * .45 !default;
 | 
			
		||||
 | 
			
		||||
// En/disable box-shadows
 | 
			
		||||
$shadows: 0 !default;
 | 
			
		||||
 | 
			
		||||
// En/disable included font
 | 
			
		||||
$use-included-font: 1 !default;
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,18 @@
 | 
			
		||||
// Overwrite normal style (colors) - shadows are ignored
 | 
			
		||||
// Overwrite normal style (colors)
 | 
			
		||||
@import 'modules/variables';
 | 
			
		||||
@import 'custom/variables';
 | 
			
		||||
 | 
			
		||||
$color-white: #111;
 | 
			
		||||
$color-white: #1c1c13;
 | 
			
		||||
$color-black: #fefefe;
 | 
			
		||||
$color-map-background: #0d151c;
 | 
			
		||||
 | 
			
		||||
$color-online: lighten($color-online, 25%);
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
  //@import 'modules/base';
 | 
			
		||||
  body {
 | 
			
		||||
  body,
 | 
			
		||||
  textarea,
 | 
			
		||||
  input {
 | 
			
		||||
    background: $color-white;
 | 
			
		||||
    color: lighten($color-black, 100);
 | 
			
		||||
  }
 | 
			
		||||
@ -18,6 +22,15 @@ html {
 | 
			
		||||
    border-bottom-color: lighten($color-white, 10%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    color: $color-online;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
 | 
			
		||||
    &:focus {
 | 
			
		||||
      color: darken($color-online, 15%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //@import 'modules/leaflet';
 | 
			
		||||
  .leaflet-container {
 | 
			
		||||
    background: $color-map-background;
 | 
			
		||||
 | 
			
		||||