sigi 2 weeks ago
commit
ac08bfbb39
53 changed files with 2859 additions and 0 deletions
  1. 80 0
      INSTALL.txt
  2. 786 0
      PingPanel.py
  3. 13 0
      changelog
  4. 95 0
      config.cfg
  5. 6 0
      contrib/PingPanel/DEBIAN/README.Debian
  6. 14 0
      contrib/PingPanel/DEBIAN/control
  7. 3 0
      contrib/PingPanel/DEBIAN/pingpanel.conffiles
  8. 13 0
      contrib/PingPanel/DEBIAN/postinst
  9. 12 0
      contrib/PingPanel/DEBIAN/preinst
  10. 14 0
      contrib/PingPanel/etc/default/pingpanel
  11. 79 0
      contrib/PingPanel/etc/init.d/pingpanel
  12. 80 0
      contrib/PingPanel/opt/PingPanel/INSTALL.txt
  13. 786 0
      contrib/PingPanel/opt/PingPanel/PingPanel.py
  14. 13 0
      contrib/PingPanel/opt/PingPanel/changelog
  15. 95 0
      contrib/PingPanel/opt/PingPanel/config.cfg
  16. 6 0
      contrib/PingPanel/opt/PingPanel/create_server-certificate.sh
  17. 14 0
      contrib/PingPanel/opt/PingPanel/etc.example/default/pingpanel
  18. 79 0
      contrib/PingPanel/opt/PingPanel/etc.example/init.d/pingpanel
  19. BIN
      contrib/PingPanel/opt/PingPanel/favicon.ico
  20. BIN
      contrib/PingPanel/opt/PingPanel/green_on.gif
  21. BIN
      contrib/PingPanel/opt/PingPanel/red_anim.gif
  22. BIN
      contrib/PingPanel/opt/PingPanel/red_on.gif
  23. 20 0
      contrib/PingPanel/opt/PingPanel/server.crt
  24. 28 0
      contrib/PingPanel/opt/PingPanel/server.key
  25. BIN
      contrib/PingPanel/opt/PingPanel/stat-inf.gif
  26. 236 0
      contrib/PingPanel/opt/PingPanel/w3.css
  27. BIN
      contrib/PingPanel/opt/PingPanel/yellow_on.gif
  28. BIN
      contrib/pingpanel_0.9.1_all.deb
  29. BIN
      contrib/pingpanel_0.9.2.tar.gz
  30. BIN
      contrib/pingpanel_0.9.2_all.deb
  31. BIN
      contrib/pingpanel_0.9.3_all.deb
  32. BIN
      contrib/pingpanel_0.9.4_all.deb
  33. BIN
      contrib/pingpanel_0.9.5.tar.gz
  34. BIN
      contrib/pingpanel_0.9.5_all.deb
  35. BIN
      contrib/pingpanel_0.9.6.tar.gz
  36. BIN
      contrib/pingpanel_0.9.6_all.deb
  37. BIN
      contrib/pingpanel_0.9.7.tar.gz
  38. BIN
      contrib/pingpanel_0.9.7_all.deb
  39. BIN
      contrib/pingpanel_0.9.8.tar.gz
  40. BIN
      contrib/pingpanel_0.9.8_all.deb
  41. 4 0
      contrib/si_README.txt
  42. 6 0
      create_server-certificate.sh
  43. 14 0
      etc.example/default/pingpanel
  44. 79 0
      etc.example/init.d/pingpanel
  45. BIN
      favicon.ico
  46. BIN
      green_on.gif
  47. BIN
      red_anim.gif
  48. BIN
      red_on.gif
  49. 20 0
      server.crt
  50. 28 0
      server.key
  51. BIN
      stat-inf.gif
  52. 236 0
      w3.css
  53. BIN
      yellow_on.gif

+ 80 - 0
INSTALL.txt

@@ -0,0 +1,80 @@
+* PingPanel INSTALL.txt (C) 2024 by Siegrist(SystemLösungen) <pingpanel@zweiernet.ch>
+
+INSTALL
+========
+
+On Debian/Ubuntu related systems install with:
+----------------------------------------------
+- #dpkg -i  pingpanel_<version>_all.deb
+
+You may encounter problems like:
+	dpkg: dependency problems prevent configuration of pingpanel:
+	 pingpanel depends on python3-tornado (>= 6.0); however:
+	  Package python3-tornado is not installed.
+	 pingpanel depends on fping (>= 5.0); however:
+	  Package fping is not installed.
+	  
+Then, check the dependencies with:
+- #apt-get check
+and install the dependencies with:
+- #apt --fix-broken install
+Afterwards the pingpanel package ist fully instsalled under /opt/PingPanel/.
+
+The daemon start file pingpanel in the init.d/ directory is in the standard LSB format for SysV systems.
+So, on systemd related systems at least use:
+- #systemctl daemon-reload
+
+After all, start the PingPanel Daemon:
+- #service pingpanel start
+
+
+
+On other systems using the tar-archive:
+---------------------------------------
+- #cd /opt
+- #tar xvfz pingpanel_<version>.tar.gz
+
+Requirements:
+- python3-tornado >= 6.0 (https://www.tornadoweb.org/en/stable/)
+- fping >= 5.0 (https://fping.org/)
+
+The directory ./etc.example contains startup file in ./init.d and a default pingpanel file under ./default.
+If you want to use a different installation directory, you must adjust the default file 
+and/or the init.d startup script to the appropriate pathes and names.
+
+After all, start the PingPanel Daemon:
+- #/etc/init.d/pingpanel start
+
+
+Configuration
+-------------
+The configuration is made in the config.cfg file located in the installation directory.
+All parameters are documented in this file.
+If you want to use a name other than config.cfg, you can specify this in the /etc/default/pingpanel file
+or with the -c <filename> param to the PingPanel.py script.
+
+All Messages are written to the file /var/log/pingpanel.log, unless 
+you define a different name in /etc/default/pingpanel.
+
+
+Upgrade
+========
+
+On Debian/Ubuntu related systems upgrade with:
+----------------------------------------------
+- #dpkg -i  pingpanel_<version>_all.deb
+
+The configuration files (config.cfg and /etc/default/pingpanel) will be preserved.
+New files will be given the prefix .dpkg-NEW.
+Adjust your config.cfg file with the new paramameters from .dpkg-NEW.
+
+Restart the PingPanel Daemon.
+
+
+On other systems using the tar-archive:
+---------------------------------------
+Install the tar-file in a new empty directory and then copy the relevant files to your active install-dir.
+
+
+
+sigi

+ 786 - 0
PingPanel.py

@@ -0,0 +1,786 @@
+#!/usr/bin/env python3
+
+# Copyright 2024 by Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+
+#import datetime
+from datetime import datetime
+import os
+import errno
+import configparser
+import time
+import signal
+import sys
+import subprocess
+import re
+import argparse
+from threading import Thread
+import json
+
+import tornado.ioloop
+import tornado.web
+import tornado.websocket
+from tornado.gen import sleep
+import asyncio
+
+VERSION = '0.9.8'
+
+konfig_file = 'config.cfg'
+s_certfile = 'server.crt'
+s_keyfile = 'server.key'
+ws_method = 'ws'
+host_dict = dict()
+failed_pingc = dict()
+loop_interval = 3
+listen_port = 8888
+listen_addr = '0.0.0.0'
+anz_hosts = 0
+
+fping_parameter = []
+fping_cmd = 'fping'
+
+def signal_handler(signal, frame):
+	print('\nCtrl+C, keyboardInterrupt detected!')
+	sys.exit(0) 
+signal.signal(signal.SIGINT, signal_handler)
+
+
+config = configparser.ConfigParser(delimiters=('='), comment_prefixes=('#'), allow_no_value=False, strict=True, empty_lines_in_values=False)
+def read_config():
+	try:
+		if config.read([konfig_file]) != []:
+			pass
+		else:
+			print("Configfile '" + konfig_file + "' not found!")
+			sys.exit()
+	except configparser.DuplicateSectionError as e:
+		print("Error: Duplicate Section '%s' not allowed." % str(e.section))
+		sys.exit()
+	except configparser.DuplicateOptionError as e:
+		print("Error: Duplicate Option '%s' in Section '%s' not allowed." % ( str(e.option), str(e.section) ))
+		sys.exit()
+	except configparser.Error as e:
+		print("Error in Configfile: %s." % str(e))
+		sys.exit()
+	
+	#print(config.sections())
+	global anz_hosts
+	for sect in config.sections():
+		for key in config[sect]:
+			if (sect == "Main"):
+				try:
+					global loop_interval
+					loop_interval = int(config["Main"]["LoopInterval"])
+				except KeyError:
+					pass
+				try:
+					global listen_port
+					listen_port = int(config["Main"]["ListenPort"])
+				except KeyError:
+					pass
+				try:
+					global listen_addr
+					listen_addr = str(config["Main"]["ListenAddr"])
+				except KeyError:
+					pass
+				try:
+					listen_addr = str(config["Main"]["TitleAdd"])
+				except KeyError:
+					config["Main"]["TitleAdd"] = ''
+				try:
+					global ws_method
+					if ( str(config["Main"]["UseHTTPS"]) ):
+						ws_method = 'wss'
+						s_certfile = str(config["Main"]["CertFile"])
+						s_keyfile = str(config["Main"]["KeyFile"])
+						apath = os.path.dirname(os.path.abspath(__file__))
+						if not file_exists(s_certfile):
+							print("CertFile not readable or does not exist: %s" % apath + '/' + s_certfile)
+							sys.exit()
+						if not file_exists(s_keyfile):
+							print("KeyFile not readable or does not exist: %s" % apath + '/' + s_keyfile)
+							sys.exit()
+				except KeyError:
+					pass
+			else:
+				#host_dict[key] = config[sect][key]
+				host_dict[key] = sect.replace(' ','_')
+				failed_pingc[key] = 0
+				anz_hosts += 1
+			if args.v: print(sect + ": " + key + " = " + config[sect][key])
+	print("Pinging " + str(anz_hosts) + " Devices in total.")
+	
+def cmd_exists(cmd):
+	path = os.environ["PATH"].split(os.pathsep)
+
+	for prefix in path:
+		filename = os.path.join(prefix, cmd)
+		executable = os.access(filename, os.X_OK)
+		is_not_directory = os.path.isfile(filename)
+		if executable and is_not_directory:
+			return True
+	return False
+
+def file_exists(file):
+	filename = os.path.dirname(os.path.abspath(__file__)) + '/' + file
+	if os.access(filename, os.R_OK):
+		return True
+	return False
+
+
+def create_fpingparam():
+	global fping_parameter
+	global fping_cmd
+	
+	try:
+		fping_parameter.append(config["Main"]["fpingCommand"])
+		fping_cmd = config["Main"]["fpingCommand"]
+	except KeyError:
+		fping_parameter.append(fping_cmd)
+		
+	fping_parameter += ['--quiet', '--vcount=3']
+	
+	try:
+		fping_parameter.append('--interval=' + config["Main"]["Fping-interval"])
+	except KeyError:
+		fping_parameter.append('--interval=1')
+	try:
+		fping_parameter.append('--period=' + config["Main"]["Fping-period"])
+	except KeyError:
+		fping_parameter.append('--period=100')
+
+
+def ping(ip, packets=1, timeout=2):
+	comd = ['ping', '-c', str(packets), '-w', str(timeout), ip]
+	res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+	return res.returncode == 0
+	
+def fping(ips):
+	#comd = ['fping', '--quiet', '--interval=2', '--vcount=3', '--period=100'] + ips
+	comd = fping_parameter + ips
+	
+	res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)	#, universal_newlines = True)
+	if args.v:
+		print("fping finished: " + datetime.now().strftime("%H:%M:%S"))
+		if res.returncode == 2:
+			print("fping: any IP addresses were not found !")
+		elif res.returncode == 3:
+			print("fping: invalid command line arguments !")
+		elif res.returncode == 4:
+			print("fping: system call failure !")
+		
+	#print("RET:"+str(res.returncode))
+	return res.stdout.decode("utf-8")
+
+
+# Pinging Loop
+async def PingLoop():
+		print("PingLoop gestartet.")
+		# sleep for a moment
+		while True:
+			await asyncio.sleep(3)
+			# Sinnlos wenn niemend zuschaut:
+			if ( len(BroadcastHandler.clients) < 1 ):
+				continue
+			
+			for host, name in host_dict.items():
+				#print("ping host: " + host)
+				if ping(host,1,2):
+					line = {
+						"host": host, "status": "OK"
+					}
+					BroadcastHandler.broadcast(line)
+				else:
+					line = {
+						"host": host, "status": "FAILED"
+					}
+					BroadcastHandler.broadcast(line)
+		
+		# End while True
+		
+# Fping Loop
+async def FpingLoop():
+		print("FpingLoop ready.")
+		ip_list = list(host_dict.keys())
+		create_fpingparam()
+		
+		# check for ping command
+		if not cmd_exists(fping_cmd):
+			print("Exit with Error: fping command not found or not executable: '%s'" % fping_cmd)
+			sys.exit(1)
+
+		if args.v:
+			tmp_cmd = [] + fping_parameter + ip_list
+			comd_out = ' '.join(tmp_cmd)
+			comd_out.replace("'", "")
+			print('ping command used: "' + comd_out + '"')
+		
+		# The Loop
+		while True:
+			
+			# Sinnlos wenn niemend zuschaut:
+			if ( len(BroadcastHandler.clients) < 1 ):
+				await asyncio.sleep(3)
+				continue
+			
+			ac_time = datetime.now().strftime("%H:%M:%S")
+			if args.v: print("call fping: " + ac_time)
+			ret = fping(ip_list)
+			rl = ret.splitlines()
+			json_bc = []
+			for line in rl:
+				host,rtims = line.split(' : ')
+				if ( re.search(r"duplicate", rtims, flags=re.I) ):
+					#print(host+": "+rtims)
+					continue
+				host = host.strip()
+				rtims = rtims.strip()
+				ux_sect = 'ux_' + host_dict[host]
+				
+				if re.search(r"[\d\.]+ [\d\.]+ [\d\.]+", rtims):
+					#print(host+" OK")
+					json_bc.append({
+						"host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"[\d\.]+ [\d\.]+ -", rtims) or re.search(r"[\d\.]+ - [\d\.]+", rtims) or re.search(r"- [\d\.]+ [\d\.]+", rtims):
+					#print(host+" OK")
+					json_bc.append({
+						"host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"- - -", rtims):
+					#print(host+" FAILED")
+					if failed_pingc[host] < int(config["Main"]["FlashThreshold"]):
+						json_bc.append({
+							"host": host, "status": "NEWFAIL", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+						})
+						failed_pingc[host] += 1
+					else:
+						json_bc.append({
+							"host": host, "status": "FAILED", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+						})
+						failed_pingc[host] = int(config["Main"]["FlashThreshold"]) + 1
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"[\d\.]+", rtims):
+					#print(host+" HALB")
+					json_bc.append({
+						"host": host, "status": "HALB", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				else:
+					print(host+" ???????????????????")
+				
+			#print(json.dumps(json_bc, separators=(',', ':'), ensure_ascii=False))
+			BroadcastHandler.broadcast(json_bc)
+			
+			# sleep for Config-LoopInterval
+			if ( len(BroadcastHandler.clients) >= 1 ):
+				await asyncio.sleep(loop_interval)
+		# End while True
+
+
+
+class BroadcastHandler(tornado.websocket.WebSocketHandler):
+	clients = []
+
+	def open(self):
+		o_time = datetime.now().strftime("%d.%m.%y %H:%M")
+		if not re.search(r"^/websocket$", self.request.uri):
+			log_msg = '"Bad request uri"'
+			print("%s: Client %s: WebSocket Open-Error: %s " % (o_time, self.request.remote_ip, log_msg))
+			self.close(1111)
+			return
+		BroadcastHandler.clients.append(self)
+		print("%s: Client %s: WebSocket opened: %s " % (o_time, self.request.remote_ip, self.request))		# oder "self.request.remote_ip" etc. / oder "self.__dict__"
+
+	def on_close(self):
+		BroadcastHandler.clients.remove(self)
+		c_time = datetime.now().strftime("%d.%m.%y %H:%M")
+		if ( self.close_code ):
+			print("%s: Client %s: WebSocket closed: %s (close_code: %d)" % (c_time, self.request.remote_ip, self.request, self.close_code))
+		else:
+			print("%s: Client %s: WebSocket undefined closed." % (c_time, self.request.remote_ip))
+		
+
+	@classmethod
+	def broadcast(cls, message):
+		errored = []
+		for client in cls.clients:
+			#print("Broadcasted: %s" % message)
+			#print("Broadcasted: %s" % client.request.remote_ip)
+			try:
+				client.write_message(json.dumps(message, separators=(',', ':'), ensure_ascii=False))
+				#print("Broadcasted: %s" % message)
+			except tornado.websocket.WebSocketClosedError:
+				print("Error sending Broadcast message: WebSocketClosedError() raised")
+				errored.append(client)
+			except tornado.websocket.Exception as e:
+				print("Error sending message: " + str(e))
+				errored.append(client)
+				
+		for conn in errored:
+			cls.clients.remove(conn)
+
+class WWWSocketHandler(tornado.web.RequestHandler):
+	#@tornado.gen.coroutine
+	def prepare(self):
+		#if self.request.protocol == 'http':
+		#	print("mmmm" + self.request.protocol)
+		pass
+			
+	def get(self):
+		#print(self.request.headers)
+		#print(self.__dict__)
+		if not re.search(r"^/$", self.request.uri):
+			return
+		html_modal01 = create_html_modal01()
+		html_tab = create_html_table()
+		self.write(html_html % (config["Main"]["TitleAdd"], ws_method, self.request.host, html_css, html_tab, html_modal01))
+	def on_connection_close(self):
+		print("RequestHandler on_connection_close(): " + str(self.__dict__))
+
+async def test():
+	while True:
+		print("test")
+		await asyncio.sleep(5)
+		
+	
+def make_app():
+	abs_path = os.path.dirname(os.path.abspath(__file__))
+	web_app = tornado.web.Application([
+		(r"^/websocket$", BroadcastHandler),
+		(r"/", WWWSocketHandler),
+		(r'/css/(.+\.css)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+		(r'/img/(.+\.gif)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+		(r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+	],debug=False,websocket_ping_interval=10,websocket_ping_timeout=30)
+	
+	if ws_method == 'ws':
+		# http method in use:
+		print("Serving HTTP and ws.")
+		print("Listen on "+listen_addr+" Port "+str(listen_port)+" (http://"+listen_addr+":"+str(listen_port)+"/).")
+		return web_app
+	
+	# https method in use:
+	print("Serving HTTPS and wss.")
+	print("Listen on "+listen_addr+" Port "+str(listen_port)+" (https://"+listen_addr+":"+str(listen_port)+"/).")
+	http_server = tornado.httpserver.HTTPServer(web_app,
+	    ssl_options = {
+	    "certfile": abs_path + '/' + s_certfile,
+	    "keyfile": abs_path + '/' + s_keyfile,
+	    }
+	)
+	
+	return http_server
+	
+	
+async def main():
+	app = make_app()
+	try:
+		app.listen(int(listen_port), str(listen_addr))
+	except OSError as e:
+		if e.errno == errno.EADDRINUSE:
+			print("listen(): 'EADDRINUSE': " + str(e.args[1]))
+			sys.exit(1)
+		else:
+			raise
+			
+	#await asyncio.gather(PingLoop(), return_exceptions=True)
+	
+	#await asyncio.gather(PingLoop())
+	await asyncio.gather(FpingLoop())
+		
+
+
+# ------ Div -------------------------------
+
+
+def create_html_table():
+	html_tab = ""
+	html_tab += '<div id="top_header" class="top_header w3-black">'
+	html_tab += '<span class="w3-left tbars" style="text-align: left;">V'+VERSION+' by <a href="https://pss.zweiernet.ch" target="_blank">PSS</a><br><span style="font-size:1em;opacity:0.8;background-color:#4e4ec4;cursor:pointer;" onclick="document.getElementById(\'modal_01\').style.display=\'block\'">&nbsp;<span class="w3-text-white w3-monospace"><b>i</b></span> &#x2754; </span></span>'
+	html_tab += '<span class="w3-center top_head_tit">PingPanel<span class="top_head_tit_pfix"> ' + config["Main"]["TitleAdd"] + '</span></span>'
+	html_tab += '<span class="w3-right tbars"><span id="cur_actime" style="text-align: right;color: #22ff00;font-weight: bold;"> </span><br><button id="close_button" onclick="socket_close(1000)" class="sbutt">stop</button><br></span>'
+	html_tab += '</div>\n'
+	html_tab += '<div class="w3-container">'
+	for sect in config.sections():
+		if (sect == "Main"):
+			continue
+		html_tab += '<div class="w3-container" style="padding: 0;font-size:14px;">'
+		sect_x = sect.replace(' ','_')
+		clickId = 'accord_' + sect_x
+		html_tab += '<br><div onclick="Accordion(\'' + clickId + '\')" class="w3-container w3-teal" style="padding: 3px 10px;opacity: 0.9;margin-top: 5px;box-shadow: 0 2px 5px 0 #04040424,0 2px 10px 0 #040404c4;letter-spacing: 2px;cursor: pointer;">' + sect + '<span id="ux_' + sect_x + '" class="w3-card-2 w3-right"></span></div>\n'
+		html_tab += '<div id="' + clickId + '" class="w3-show">'
+		for key in config[sect]:
+			tkey = key.replace('.', '_')
+			html_tab += '<div class="w3-left w3-dark-grey w3-border w3-border-teal tooltip" style="text-align: center;margin: 1px; min-width: 13ch; padding: 4px min(3px, 1vw) !important;box-shadow:3px 2px 4px #0009 !important;">'
+			html_tab += '<span class="w3-card-2 tooltiptext">'+config[sect][key]+'<br><span id="ttip_' + tkey + '" class="ttip_small"></span></span>'
+			html_tab += '<span id="' + tkey + '" class="ip_num">' + key + '</span><br><span id="stat_' + tkey + '" class="w3-center"></span></div>'
+		html_tab += '</div>\n</div>\n'
+	html_tab += '</div>\n'
+
+	
+	return html_tab
+		
+		
+		
+
+html_html = '''
+<!doctype html>
+<html>
+    <header>
+    	<title>PingPanel %s</title>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<meta charset="UTF-8"> 
+        <script type="text/javascript">
+        	window.onload = start_websocket();
+        	function start_websocket() {
+            	var ws = new WebSocket("%s://%s/websocket");
+            	ws.onmessage = function (evt) {
+            		//let test = evt.data.replace(/'/g, '"');		// needed " by JSON.parse
+            		//var json_arr = JSON.parse(test);
+            		var json_arr = JSON.parse(evt.data);
+            		var cur_actime = "";
+            		
+            		// Colortable counters initialisation
+            		let M_red = new Map();
+            		let M_green = new Map();
+            		let M_yellow = new Map();
+            		for( let i = 0; i < json_arr.length; i++ ) {
+            			if ( M_red.has(json_arr[i].sect) ) {
+            				continue;
+            			} else {
+            				M_red.set(json_arr[i].sect, 0);
+            				M_green.set(json_arr[i].sect, 0);
+            				M_yellow.set(json_arr[i].sect, 0);
+            			}
+            		}
+
+            		// DOM Updates
+            		for(let i = 0; i < json_arr.length; i++) {
+            			var data_obj = json_arr[i];
+            			cur_actime = data_obj.actime;
+            	   		var cur_host = data_obj.host;
+            	   		var cur_status = data_obj.status;
+            	   		var cur_sect_ux = data_obj.sect;
+            	   		var cur_rtt = data_obj.rtt;
+            	   		cur_rtt = cur_rtt.replace(/ /g,' \/ ');
+            	   		var uc_cur_host = cur_host.replace(/\./g,'_');
+            	   		document.getElementById(uc_cur_host).textContent = cur_host;
+            	   		var stat_id = 'stat_' + uc_cur_host;
+            	   		var ttip_id = 'ttip_' + uc_cur_host;
+            	   		if (cur_status == 'OK') {
+            	   			M_green.set(cur_sect_ux, M_green.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/green_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else if (cur_status == 'FAILED') {
+            	   			M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/red_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else if (cur_status == 'NEWFAIL') {
+            	   			M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/red_anim.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else {
+            	   			M_yellow.set(cur_sect_ux, M_yellow.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/yellow_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		}
+            	   	} // End for
+            	   	document.getElementById('cur_actime').innerHTML = cur_actime;
+            	   	for (let [sect, val] of M_red) {
+            	   		document.getElementById(sect).innerHTML = '<span class="unreach_r"><span class="red_r">'+M_red.get(sect).toString().padStart(3, " ")+'</span> | <span class="yellow_r">'+M_yellow.get(sect).toString().padStart(3, " ")+'</span> | <span class="green_r">'+M_green.get(sect).toString().padStart(3, " ")+'</span></span>';
+            	   	}
+            	};
+            	
+            	ws.onclose = function(evt) {
+   					//alert("WebSocket closed.");
+   					document.getElementById('cur_actime').innerHTML = '<span style="color:red;">stopped</span>';
+   					document.getElementById('close_button').style.opacity = "0.3";
+   					//document.getElementById('modal_alert_text').style.display='block';
+   					//document.getElementById('modal_alert_text').innerHTML = 'WebSocket closed.';
+				};
+				ws.onerror = function(evt) {
+   					alert("WebSocket closed due to an error.");
+   					document.getElementById('cur_actime').innerHTML = '<span style="color:red;">Error</span>';
+   					document.getElementById('close_button').style.opacity = "0.3";
+				};
+				socket_close = (code) => {
+					document.getElementById('close_button').style.opacity = "0.3";
+					ws.close(code);
+				};
+            };
+            
+            function Accordion(id) {
+            	var x = document.getElementById(id);	//alert(x);   //.previousSibling.getElementsByTagName("*").item(0).innerHTML);
+				if (x.className.indexOf("w3-show") == -1) {
+					x.className = "w3-show";
+				} else { 
+					x.className = x.className.replace("w3-show", "w3-hide");
+				}
+			};
+        </script>
+        <script>
+        // modal handling
+		var modal = document.getElementById('modal_01');
+		// When the user clicks anywhere outside of the modal, close it
+		window.onclick = function(event) {
+			if (event.target == modal) {
+		    	modal.style.display = "none";
+			}
+		}
+        </script>
+        
+        <link rel="stylesheet" href="/css/w3.css">
+        <style>
+        	%s
+        </style>
+    </header>
+    <body>
+        <div id="main">%s</div>\n
+        
+        
+        <div id="modal_01" class="w3-modal modal_01" onclick="this.style.display='none'">
+        	<div class="w3-modal-content modal_01_text">
+    			<div class="w3-container">
+  					<h4 style="font-variant:small-caps;letter-spacing:3px;">PingPanel</h4>
+  					%s
+  				</div>
+  			</div>
+		</div>
+    </body>
+</html>
+'''
+
+html_css = '''
+body {
+    background: #333;
+    font-family: Verdana, Arial, sans-serif;
+    color: #ddd;
+    line-height: 110%;
+    font-size: 13px;
+}
+.red { color: red; }
+.green { color: #0f0; }
+.yellow { color: yellow; }
+.red_r { color: red; white-space: pre; text-align: right; font-family: monospace;}
+.green_r { color: #0f0; white-space: pre; text-align: right;; font-family: monospace;}
+.yellow_r { color: yellow; white-space: pre; text-align: right;; font-family: monospace;}
+.ip_num { font-size: 80%; }
+
+.top_header {
+  text-align: center;
+  padding: 5px 10px; height: 43px;
+  box-shadow: 0px 1px 9px 1px #c3d0cd78 !important;
+  position: sticky;
+  z-index: 100;
+  top: 0px;
+}
+
+.tbars {
+  color: #aaa;
+  font-size: 11px;
+  font-stretch: semi-expanded;
+}
+
+.unreach_r {
+  background-color:#313131;
+  padding-left:5px;
+  padding-right:5px;
+  letter-spacing: 0px;
+  border-radius: 2px;
+  font-weight: bold;
+  font-size: 12px;
+  font-family: Bitstream;
+}
+
+.modal_01 {
+  
+}
+.modal_01_text {
+  background-color: #000;
+  color: #f0f0ca;
+  line-height: 120%;
+  font-size: 1.1em;
+}
+
+.sbutt {
+  border: none;
+  display: inline-block;
+  vertical-align: middle;
+  overflow: hidden;
+  text-decoration: none;
+  text-align: center;
+  cursor: pointer;
+  white-space: nowrap;
+  border-radius: 4px;
+  padding: 0px 8px !important;
+  background-color: #9b9b9b !important;
+  color: #000 !important;
+}
+
+.top_head_tit {
+  font-size: 22px;
+  font-variant: small-caps;
+  letter-spacing: 4px;
+  top: 0.3em;
+  position: sticky;
+}
+.top_head_tit_pfix {
+  font-size: 0.5em;
+  font-variant: small-caps;
+  letter-spacing: 0px;
+}
+
+.ttip_small {
+  font-size: 64%;
+  text-align: center;
+  padding-top: 6px;
+}
+ 
+
+.tooltip {
+  position: relative;
+  display: inline-block;
+  border-bottom: 1px dotted black;
+}
+
+.tooltip .tooltiptext {
+  font-family: monospace;
+  visibility: hidden;
+  width: 150px;
+  background-color: black;
+  color: #fff;
+  text-align: center;
+  font-size: 15px;
+  border-radius: 6px;
+  padding: 5px 5px;
+  position: absolute;
+  z-index: 1;
+  top: 120%;
+  left: 50%;
+  margin-left: -60px;
+}
+
+.tooltip .tooltiptext::after {
+  content: "";
+  position: absolute;
+  bottom: 100%;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px;
+  border-style: solid;
+  border-color: transparent transparent black transparent;
+}
+
+.tooltip:hover .tooltiptext {
+  visibility: visible;
+}
+
+.sticky {
+  position: fixed;
+  top: 0;
+  width: 100%;
+}
+
+.sticky + .content {
+  padding-top: 50px;
+}
+
+'''
+
+def create_html_modal01():
+	html_modal = '''
+<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Info</p>
+ Config File: 
+ 	''' + konfig_file + '''<br>
+ Total number of Devices: _NUM_HOSTS_ <br>
+ LoopInterval: 
+ 	''' + str(loop_interval) + ''' s<br>
+ FlashThreshold: 
+ 	''' + str(config["Main"]["FlashThreshold"]) + ''' rounds<br>
+ fping: 
+	''' + fping_parameter[2].replace('--','') + '''<br>
+ fping: 
+	''' + fping_parameter[3].replace('--','') + ''' ms<br>
+ fping: 
+	''' + fping_parameter[4].replace('--','') + ''' ms<br>
+<br> 
+
+<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Help</p>
+This PingPanel graphical interface consists of a header and the configured sections with the corresponding devices to be monitored.<br>
+
+Among other things, the header contains the current time (<span class="green">green</span>) of the last ping run 
+or the status "stopped" (<span class="red">red</span>) if the connection to the server has been lost or the "Stop" button below has been pressed.<br>
+Pressing the "Stop" button terminates the connection to the server and, if no other client is active, the ping rounds are suspended.<p>
+
+The blue-green <span class="w3-teal">section bars</span> contain the name of the section and consolidated information about the status of the devices in this section.<br>
+<u>The section bars can be expanded and collapsed by clicking or tapping on them.</u><br>
+Consolidated status information <img src="/img/stat-inf.gif"> is divided into <span class="red">failed</span>, 
+<span class="yellow">partially answered</span> and <span class="green">answered</span> ping requests.<br>
+
+Each monitored device displays its IP address and status. By hovering over or tapping on it the corresponding comment 
+is displayed and every ping RTT of each ping round is reported.<p>
+
+A <span class="red">flashing red</span> status light indicates that the device has freshly not responded since 'FlashThreshold' ping rounds.<br>
+A <span class="red">red</span> status light indicates that the device has not answered any of the three pings.<br>
+A <span class="yellow">yellow</span> status light indicates that the device has answered one of three pings.<br>
+A <span class="green">green</span> status light indicates that the device has answered to all three or at least two pings.<br>
+<p></p>
+<p> </p>
+
+
+
+
+
+
+
+
+	'''
+
+	html_modal = html_modal.replace('_NUM_HOSTS_', str(anz_hosts))
+
+	return html_modal
+
+
+
+# -------- MAIN ------------------------------
+if __name__ == "__main__":
+	
+	print("PingPanel v"+VERSION+" by PSS -- started at " + datetime.now().strftime("%F %H:%M:%S"))
+	# Commandline parsing
+	parser = argparse.ArgumentParser(description='PingPanel V'+VERSION+"\n2024 by sigi",
+                                   formatter_class=argparse.RawDescriptionHelpFormatter)
+	parser.add_argument('-c', help="Alternative configuration file (standard: config.cfg)", type=str, metavar='config-file')
+	parser.add_argument('-v', help="Verbose mode", action="store_true")
+	args = parser.parse_args()
+	if args.c:
+		konfig_file = args.c
+	print("Configfile: " + konfig_file)
+	
+	
+	# Config
+	read_config()
+	
+	
+	# create the app
+	asyncio.run(main())
+
+
+# ------
+
+

+ 13 - 0
changelog

@@ -0,0 +1,13 @@
+v0.9.6 / 18.7.24, sigi:
+	- Corr: handling duplicate ping answers (ICMP replys).
+	- formatting status information fileds.
+	
+v0.9.7 / 30.7.24, sigi:
+	- Acceleration: relocation of json handling from frontend to backend.
+	- some CSS cosmetics.
+	
+v0.9.8 / 12.8.24, sigi:
+	- implementing secure HTTPS connections on both, webserver and websocket.
+	- Added HTTPS params in config.cfg.
+	- Changed: 'Title' param renamed to 'TitleAdd' in config.cfg as it is now an addendum to the title.
+	

+ 95 - 0
config.cfg

@@ -0,0 +1,95 @@
+#-- Main Section:
+## ! Do not change nor delete the Main-Section name ([Main]) !
+## ! Do not use quotation marks, neither for keys nor for values !
+[Main]
+
+## The title addendum to PingPanel on top of the site and in the browser window title.
+## Pay attention to the length when using on smartphones.
+TitleAdd = 
+
+## Port the Webserver listen on:
+ListenPort = 8888
+
+## Address the Webserver is bound on:
+ListenAddr = 0.0.0.0
+
+## Using secure HTTPS conections:
+## Using HTTPS, comment out the following three parameters.
+## Of course, the file names are freely choosable here, relative to the current directory.
+#UseHTTPS = true
+#CertFile = server.crt
+#KeyFile = server.key
+
+
+## Location of the 'fping' command, without any quotation marks (defaults to: fping)
+fpingCommand = fping
+
+#-- fping related parameters (see https://fping.org/fping.8.html):
+## Fping-interval:
+##- from manpage - The minimum amount of time (in milliseconds) between sending a ping packet to any target
+##- from manpage - (default is 10, minimum is 1).
+## Increasing this value has a major impact on the runtime of a single ping round when a high number of hosts are pinged.
+## PingPanel default is 1.
+Fping-interval = 1
+
+## Fping-period:
+##- from manpage - This parameter sets the time in milliseconds that fping waits between successive
+##- from manpage - packets to an individual target. Default is 1000 and minimum is 10.
+## PingPanel default is 100.
+Fping-period = 100
+
+#-- End fping related params
+
+
+## LoopInterval:
+## The time that the program sleeps (in seconds) between ping runs (default is 3).
+## Since the ping is executed asynchronously, this LoopInterval is the waiting time between two completed
+## consecutive ping subprocesses. The time required by the ping subprocess itself depends on the number of hosts
+## and the fping parameters defined above. Tip: fiddle around with the shown fping command (argument PingPanel -v)
+## within the Linux 'time' command to find the optimal value for your needs.
+LoopInterval = 3
+
+## FlashThreshold:
+## Number of ping rounds for which a host does not respond until the status light changes from flashing red to permanently red.
+FlashThreshold = 3
+
+
+#-- User specified sections follow
+## The following sections consist of a section name followed by IP-Addresses with an associated comment.
+## The section name must be in square brackets and is case-sensitive. At least one section name must exist.
+## IP-Addresses can be in IPv4 or in IPv6 format and are delimited by an equal-sign (=) from comment.
+## Everything after the equal-sign is considered a comment. HTML-Tags are allowed in comment, but be careful
+## not to overload the page.
+## The comment can be omitted.
+## Leading and trailing whitespace is removed from comment and IP-Address by the program.
+[LOCAL]
+192.168.1.1		= One
+192.168.1.2		= Two
+192.168.1.3		= Three
+192.168.1.4		= Four
+192.168.1.7		= Seven
+192.168.1.8		= Eight
+192.168.1.9	= 
+192.168.1.10	= Router Number Ten
+192.168.1.11	= Switch Number Eleven
+
+
+[Zum Fröhlichen Zehnernetz]
+10.0.0.1    = happy 1
+10.0.0.2    = happy 2
+10.0.0.3    = happy 3
+10.0.0.4    = happy 4
+10.0.0.5    = happy 5
+10.0.1.100  = 
+
+
+[REMOTE]
+212.51.140.225			= <span style="color: yellow">ZweierNet.ch</span>
+2a02:168:6229::10		= <span style="color: blue">ZweierNet.ch IPv6</span>
+8.8.8.8					= The Quad Eight
+2001:4860:4860::8888 	= Quad Eight IPv6
+9.9.9.9					= The Nines
+2620:fe::9				= The Nines IPv6
+1.1.1.1					= The One and One and One and One
+2606:4700:4700::1111 	= The Ones IPv6
+

+ 6 - 0
contrib/PingPanel/DEBIAN/README.Debian

@@ -0,0 +1,6 @@
+pingpanel for Debian
+-------------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>  Tue, 12 Mar 2024 17:20:53 +0100

+ 14 - 0
contrib/PingPanel/DEBIAN/control

@@ -0,0 +1,14 @@
+Source: pingpanel
+Section: net
+Priority: optional
+Maintainer: Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>
+Version: 0.9.8
+Homepage: http://pingpanel.ZweierNet.ch
+Rules-Requires-Root: no
+Package: pingpanel
+Architecture: all
+Depends: python3 (>= 3.6), python3-tornado (>= 6.0), fping (>= 5.0)
+Description: PingPanel monitoring of network devices
+ PingPanel is a utility for monitoring network devices
+ and provides a web-based service, the panel, for 
+ displaying the captured statuses.

+ 3 - 0
contrib/PingPanel/DEBIAN/pingpanel.conffiles

@@ -0,0 +1,3 @@
+/etc/default/pingpanel
+/opt/PingPanel/config.cfg
+

+ 13 - 0
contrib/PingPanel/DEBIAN/postinst

@@ -0,0 +1,13 @@
+#!/bin/sh
+set -e
+
+if [ -f /opt/PingPanel/config.cfg.TMPINST ]
+	then
+		cp --preserve=all /opt/PingPanel/config.cfg /opt/PingPanel/config.cfg.dpkg-NEW
+		mv /opt/PingPanel/config.cfg.TMPINST /opt/PingPanel/config.cfg
+fi
+
+if [ -f /etc/default/pingpanel.TMPINST ]
+    then
+        mv /etc/default/pingpanel.TMPINST /etc/default/pingpanel
+fi

+ 12 - 0
contrib/PingPanel/DEBIAN/preinst

@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -f /opt/PingPanel/config.cfg ]
+	then
+		cp --preserve=all /opt/PingPanel/config.cfg /opt/PingPanel/config.cfg.TMPINST
+fi
+
+if [ -f /etc/default/pingpanel ]
+    then
+        cp --preserve=all /etc/default/pingpanel /etc/default/pingpanel.TMPINST
+fi

+ 14 - 0
contrib/PingPanel/etc/default/pingpanel

@@ -0,0 +1,14 @@
+# /etc/default/pingpanel
+
+# Defaults for pingpanel initscript. This file is sourced by /etc/init.d/pingpanel.
+# all vars used in /etc/init.d/pingpanel can be set here.
+#
+
+# Configfile (Standard: config.cfg)
+CONFIG=/opt/PingPanel/config.cfg
+
+# the path to logfile
+LOGFILE=/var/log/pingpanel.log
+
+# Options to pass to PingPanel.py
+#PINGPANEL_OPTS=""

+ 79 - 0
contrib/PingPanel/etc/init.d/pingpanel

@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# /etc/init.d/pingpanel                  Startup script for the PingPanel process
+#
+### BEGIN INIT INFO
+# Provides:          pingpanel
+# Required-Start:    $remote_fs
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: PingPanel monitoring of network devices
+# Description:       PingPanel is a utility for monitoring network devices
+#                    and provides a web-based service, the panel, for
+#                                        displaying the captured statuses.
+### END INIT INFO
+
+. /lib/lsb/init-functions
+
+DAEMON=/opt/PingPanel/PingPanel.py
+CONFIG=/opt/PingPanel/config.cfg
+NAME=pingpanel
+DESC="PingPanel monitor service"
+LOGFILE=/var/log/pingpanel.log
+PINGPANEL_OPTS=
+
+set -e
+
+# Check if DAEMON binary exist
+[ -f $DAEMON ] || exit 0
+
+[ -f "/etc/default/$NAME" ] && . /etc/default/$NAME
+
+PINGPANEL_OPTS="-c $CONFIG $PINGPANEL_OPTS"
+
+case "$1" in
+  start)
+    log_daemon_msg "Starting $DESC" "$NAME"
+	
+	start_daemon /usr/bin/python3 -u $DAEMON $PINGPANEL_OPTS 1>>$LOGFILE 2>>$LOGFILE &
+
+    if pgrep -f $DAEMON
+    then
+      log_success_msg "$DAEMON running."
+    else
+      log_failure_msg "$DAEMON start FAILED!"
+    fi
+    ;;
+  stop)
+    log_daemon_msg "Stopping $DESC" "$NAME"
+
+    kill -9 $(pgrep -f $DAEMON)
+    if pgrep -f $DAEMON
+    then
+      log_end_msg 1
+    else
+      log_end_msg 0
+    fi
+    ;;
+  restart)
+    $0 stop
+    sleep 2
+    $0 start
+    ;;
+  status)
+    if pgrep -f $DAEMON
+	then
+      log_end_msg 0
+      exit 0
+    else
+      log_end_msg 1
+      exit 1
+    fi
+    ;;
+  *)
+    log_action_msg "Usage: /etc/init.d/$NAME {start|stop|restart|status}"
+    ;;
+esac
+
+exit 0

+ 80 - 0
contrib/PingPanel/opt/PingPanel/INSTALL.txt

@@ -0,0 +1,80 @@
+* PingPanel INSTALL.txt (C) 2024 by Siegrist(SystemLösungen) <pingpanel@zweiernet.ch>
+
+INSTALL
+========
+
+On Debian/Ubuntu related systems install with:
+----------------------------------------------
+- #dpkg -i  pingpanel_<version>_all.deb
+
+You may encounter problems like:
+	dpkg: dependency problems prevent configuration of pingpanel:
+	 pingpanel depends on python3-tornado (>= 6.0); however:
+	  Package python3-tornado is not installed.
+	 pingpanel depends on fping (>= 5.0); however:
+	  Package fping is not installed.
+	  
+Then, check the dependencies with:
+- #apt-get check
+and install the dependencies with:
+- #apt --fix-broken install
+Afterwards the pingpanel package ist fully instsalled under /opt/PingPanel/.
+
+The daemon start file pingpanel in the init.d/ directory is in the standard LSB format for SysV systems.
+So, on systemd related systems at least use:
+- #systemctl daemon-reload
+
+After all, start the PingPanel Daemon:
+- #service pingpanel start
+
+
+
+On other systems using the tar-archive:
+---------------------------------------
+- #cd /opt
+- #tar xvfz pingpanel_<version>.tar.gz
+
+Requirements:
+- python3-tornado >= 6.0 (https://www.tornadoweb.org/en/stable/)
+- fping >= 5.0 (https://fping.org/)
+
+The directory ./etc.example contains startup file in ./init.d and a default pingpanel file under ./default.
+If you want to use a different installation directory, you must adjust the default file 
+and/or the init.d startup script to the appropriate pathes and names.
+
+After all, start the PingPanel Daemon:
+- #/etc/init.d/pingpanel start
+
+
+Configuration
+-------------
+The configuration is made in the config.cfg file located in the installation directory.
+All parameters are documented in this file.
+If you want to use a name other than config.cfg, you can specify this in the /etc/default/pingpanel file
+or with the -c <filename> param to the PingPanel.py script.
+
+All Messages are written to the file /var/log/pingpanel.log, unless 
+you define a different name in /etc/default/pingpanel.
+
+
+Upgrade
+========
+
+On Debian/Ubuntu related systems upgrade with:
+----------------------------------------------
+- #dpkg -i  pingpanel_<version>_all.deb
+
+The configuration files (config.cfg and /etc/default/pingpanel) will be preserved.
+New files will be given the prefix .dpkg-NEW.
+Adjust your config.cfg file with the new paramameters from .dpkg-NEW.
+
+Restart the PingPanel Daemon.
+
+
+On other systems using the tar-archive:
+---------------------------------------
+Install the tar-file in a new empty directory and then copy the relevant files to your active install-dir.
+
+
+
+sigi

+ 786 - 0
contrib/PingPanel/opt/PingPanel/PingPanel.py

@@ -0,0 +1,786 @@
+#!/usr/bin/env python3
+
+# Copyright 2024 by Siegrist(SystemLoesungen) <PSS@ZweierNet.ch>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+
+#import datetime
+from datetime import datetime
+import os
+import errno
+import configparser
+import time
+import signal
+import sys
+import subprocess
+import re
+import argparse
+from threading import Thread
+import json
+
+import tornado.ioloop
+import tornado.web
+import tornado.websocket
+from tornado.gen import sleep
+import asyncio
+
+VERSION = '0.9.8'
+
+konfig_file = 'config.cfg'
+s_certfile = 'server.crt'
+s_keyfile = 'server.key'
+ws_method = 'ws'
+host_dict = dict()
+failed_pingc = dict()
+loop_interval = 3
+listen_port = 8888
+listen_addr = '0.0.0.0'
+anz_hosts = 0
+
+fping_parameter = []
+fping_cmd = 'fping'
+
+def signal_handler(signal, frame):
+	print('\nCtrl+C, keyboardInterrupt detected!')
+	sys.exit(0) 
+signal.signal(signal.SIGINT, signal_handler)
+
+
+config = configparser.ConfigParser(delimiters=('='), comment_prefixes=('#'), allow_no_value=False, strict=True, empty_lines_in_values=False)
+def read_config():
+	try:
+		if config.read([konfig_file]) != []:
+			pass
+		else:
+			print("Configfile '" + konfig_file + "' not found!")
+			sys.exit()
+	except configparser.DuplicateSectionError as e:
+		print("Error: Duplicate Section '%s' not allowed." % str(e.section))
+		sys.exit()
+	except configparser.DuplicateOptionError as e:
+		print("Error: Duplicate Option '%s' in Section '%s' not allowed." % ( str(e.option), str(e.section) ))
+		sys.exit()
+	except configparser.Error as e:
+		print("Error in Configfile: %s." % str(e))
+		sys.exit()
+	
+	#print(config.sections())
+	global anz_hosts
+	for sect in config.sections():
+		for key in config[sect]:
+			if (sect == "Main"):
+				try:
+					global loop_interval
+					loop_interval = int(config["Main"]["LoopInterval"])
+				except KeyError:
+					pass
+				try:
+					global listen_port
+					listen_port = int(config["Main"]["ListenPort"])
+				except KeyError:
+					pass
+				try:
+					global listen_addr
+					listen_addr = str(config["Main"]["ListenAddr"])
+				except KeyError:
+					pass
+				try:
+					listen_addr = str(config["Main"]["TitleAdd"])
+				except KeyError:
+					config["Main"]["TitleAdd"] = ''
+				try:
+					global ws_method
+					if ( str(config["Main"]["UseHTTPS"]) ):
+						ws_method = 'wss'
+						s_certfile = str(config["Main"]["CertFile"])
+						s_keyfile = str(config["Main"]["KeyFile"])
+						apath = os.path.dirname(os.path.abspath(__file__))
+						if not file_exists(s_certfile):
+							print("CertFile not readable or does not exist: %s" % apath + '/' + s_certfile)
+							sys.exit()
+						if not file_exists(s_keyfile):
+							print("KeyFile not readable or does not exist: %s" % apath + '/' + s_keyfile)
+							sys.exit()
+				except KeyError:
+					pass
+			else:
+				#host_dict[key] = config[sect][key]
+				host_dict[key] = sect.replace(' ','_')
+				failed_pingc[key] = 0
+				anz_hosts += 1
+			if args.v: print(sect + ": " + key + " = " + config[sect][key])
+	print("Pinging " + str(anz_hosts) + " Devices in total.")
+	
+def cmd_exists(cmd):
+	path = os.environ["PATH"].split(os.pathsep)
+
+	for prefix in path:
+		filename = os.path.join(prefix, cmd)
+		executable = os.access(filename, os.X_OK)
+		is_not_directory = os.path.isfile(filename)
+		if executable and is_not_directory:
+			return True
+	return False
+
+def file_exists(file):
+	filename = os.path.dirname(os.path.abspath(__file__)) + '/' + file
+	if os.access(filename, os.R_OK):
+		return True
+	return False
+
+
+def create_fpingparam():
+	global fping_parameter
+	global fping_cmd
+	
+	try:
+		fping_parameter.append(config["Main"]["fpingCommand"])
+		fping_cmd = config["Main"]["fpingCommand"]
+	except KeyError:
+		fping_parameter.append(fping_cmd)
+		
+	fping_parameter += ['--quiet', '--vcount=3']
+	
+	try:
+		fping_parameter.append('--interval=' + config["Main"]["Fping-interval"])
+	except KeyError:
+		fping_parameter.append('--interval=1')
+	try:
+		fping_parameter.append('--period=' + config["Main"]["Fping-period"])
+	except KeyError:
+		fping_parameter.append('--period=100')
+
+
+def ping(ip, packets=1, timeout=2):
+	comd = ['ping', '-c', str(packets), '-w', str(timeout), ip]
+	res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+	return res.returncode == 0
+	
+def fping(ips):
+	#comd = ['fping', '--quiet', '--interval=2', '--vcount=3', '--period=100'] + ips
+	comd = fping_parameter + ips
+	
+	res = subprocess.run(comd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)	#, universal_newlines = True)
+	if args.v:
+		print("fping finished: " + datetime.now().strftime("%H:%M:%S"))
+		if res.returncode == 2:
+			print("fping: any IP addresses were not found !")
+		elif res.returncode == 3:
+			print("fping: invalid command line arguments !")
+		elif res.returncode == 4:
+			print("fping: system call failure !")
+		
+	#print("RET:"+str(res.returncode))
+	return res.stdout.decode("utf-8")
+
+
+# Pinging Loop
+async def PingLoop():
+		print("PingLoop gestartet.")
+		# sleep for a moment
+		while True:
+			await asyncio.sleep(3)
+			# Sinnlos wenn niemend zuschaut:
+			if ( len(BroadcastHandler.clients) < 1 ):
+				continue
+			
+			for host, name in host_dict.items():
+				#print("ping host: " + host)
+				if ping(host,1,2):
+					line = {
+						"host": host, "status": "OK"
+					}
+					BroadcastHandler.broadcast(line)
+				else:
+					line = {
+						"host": host, "status": "FAILED"
+					}
+					BroadcastHandler.broadcast(line)
+		
+		# End while True
+		
+# Fping Loop
+async def FpingLoop():
+		print("FpingLoop ready.")
+		ip_list = list(host_dict.keys())
+		create_fpingparam()
+		
+		# check for ping command
+		if not cmd_exists(fping_cmd):
+			print("Exit with Error: fping command not found or not executable: '%s'" % fping_cmd)
+			sys.exit(1)
+
+		if args.v:
+			tmp_cmd = [] + fping_parameter + ip_list
+			comd_out = ' '.join(tmp_cmd)
+			comd_out.replace("'", "")
+			print('ping command used: "' + comd_out + '"')
+		
+		# The Loop
+		while True:
+			
+			# Sinnlos wenn niemend zuschaut:
+			if ( len(BroadcastHandler.clients) < 1 ):
+				await asyncio.sleep(3)
+				continue
+			
+			ac_time = datetime.now().strftime("%H:%M:%S")
+			if args.v: print("call fping: " + ac_time)
+			ret = fping(ip_list)
+			rl = ret.splitlines()
+			json_bc = []
+			for line in rl:
+				host,rtims = line.split(' : ')
+				if ( re.search(r"duplicate", rtims, flags=re.I) ):
+					#print(host+": "+rtims)
+					continue
+				host = host.strip()
+				rtims = rtims.strip()
+				ux_sect = 'ux_' + host_dict[host]
+				
+				if re.search(r"[\d\.]+ [\d\.]+ [\d\.]+", rtims):
+					#print(host+" OK")
+					json_bc.append({
+						"host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"[\d\.]+ [\d\.]+ -", rtims) or re.search(r"[\d\.]+ - [\d\.]+", rtims) or re.search(r"- [\d\.]+ [\d\.]+", rtims):
+					#print(host+" OK")
+					json_bc.append({
+						"host": host, "status": "OK", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"- - -", rtims):
+					#print(host+" FAILED")
+					if failed_pingc[host] < int(config["Main"]["FlashThreshold"]):
+						json_bc.append({
+							"host": host, "status": "NEWFAIL", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+						})
+						failed_pingc[host] += 1
+					else:
+						json_bc.append({
+							"host": host, "status": "FAILED", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+						})
+						failed_pingc[host] = int(config["Main"]["FlashThreshold"]) + 1
+					#BroadcastHandler.broadcast(line)
+				elif re.search(r"[\d\.]+", rtims):
+					#print(host+" HALB")
+					json_bc.append({
+						"host": host, "status": "HALB", "rtt": rtims, "actime": ac_time, "sect": ux_sect
+					})
+					failed_pingc[host] = 0
+					#BroadcastHandler.broadcast(line)
+				else:
+					print(host+" ???????????????????")
+				
+			#print(json.dumps(json_bc, separators=(',', ':'), ensure_ascii=False))
+			BroadcastHandler.broadcast(json_bc)
+			
+			# sleep for Config-LoopInterval
+			if ( len(BroadcastHandler.clients) >= 1 ):
+				await asyncio.sleep(loop_interval)
+		# End while True
+
+
+
+class BroadcastHandler(tornado.websocket.WebSocketHandler):
+	clients = []
+
+	def open(self):
+		o_time = datetime.now().strftime("%d.%m.%y %H:%M")
+		if not re.search(r"^/websocket$", self.request.uri):
+			log_msg = '"Bad request uri"'
+			print("%s: Client %s: WebSocket Open-Error: %s " % (o_time, self.request.remote_ip, log_msg))
+			self.close(1111)
+			return
+		BroadcastHandler.clients.append(self)
+		print("%s: Client %s: WebSocket opened: %s " % (o_time, self.request.remote_ip, self.request))		# oder "self.request.remote_ip" etc. / oder "self.__dict__"
+
+	def on_close(self):
+		BroadcastHandler.clients.remove(self)
+		c_time = datetime.now().strftime("%d.%m.%y %H:%M")
+		if ( self.close_code ):
+			print("%s: Client %s: WebSocket closed: %s (close_code: %d)" % (c_time, self.request.remote_ip, self.request, self.close_code))
+		else:
+			print("%s: Client %s: WebSocket undefined closed." % (c_time, self.request.remote_ip))
+		
+
+	@classmethod
+	def broadcast(cls, message):
+		errored = []
+		for client in cls.clients:
+			#print("Broadcasted: %s" % message)
+			#print("Broadcasted: %s" % client.request.remote_ip)
+			try:
+				client.write_message(json.dumps(message, separators=(',', ':'), ensure_ascii=False))
+				#print("Broadcasted: %s" % message)
+			except tornado.websocket.WebSocketClosedError:
+				print("Error sending Broadcast message: WebSocketClosedError() raised")
+				errored.append(client)
+			except tornado.websocket.Exception as e:
+				print("Error sending message: " + str(e))
+				errored.append(client)
+				
+		for conn in errored:
+			cls.clients.remove(conn)
+
+class WWWSocketHandler(tornado.web.RequestHandler):
+	#@tornado.gen.coroutine
+	def prepare(self):
+		#if self.request.protocol == 'http':
+		#	print("mmmm" + self.request.protocol)
+		pass
+			
+	def get(self):
+		#print(self.request.headers)
+		#print(self.__dict__)
+		if not re.search(r"^/$", self.request.uri):
+			return
+		html_modal01 = create_html_modal01()
+		html_tab = create_html_table()
+		self.write(html_html % (config["Main"]["TitleAdd"], ws_method, self.request.host, html_css, html_tab, html_modal01))
+	def on_connection_close(self):
+		print("RequestHandler on_connection_close(): " + str(self.__dict__))
+
+async def test():
+	while True:
+		print("test")
+		await asyncio.sleep(5)
+		
+	
+def make_app():
+	abs_path = os.path.dirname(os.path.abspath(__file__))
+	web_app = tornado.web.Application([
+		(r"^/websocket$", BroadcastHandler),
+		(r"/", WWWSocketHandler),
+		(r'/css/(.+\.css)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+		(r'/img/(.+\.gif)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+		(r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path': abs_path + '/'}),
+	],debug=False,websocket_ping_interval=10,websocket_ping_timeout=30)
+	
+	if ws_method == 'ws':
+		# http method in use:
+		print("Serving HTTP and ws.")
+		print("Listen on "+listen_addr+" Port "+str(listen_port)+" (http://"+listen_addr+":"+str(listen_port)+"/).")
+		return web_app
+	
+	# https method in use:
+	print("Serving HTTPS and wss.")
+	print("Listen on "+listen_addr+" Port "+str(listen_port)+" (https://"+listen_addr+":"+str(listen_port)+"/).")
+	http_server = tornado.httpserver.HTTPServer(web_app,
+	    ssl_options = {
+	    "certfile": abs_path + '/' + s_certfile,
+	    "keyfile": abs_path + '/' + s_keyfile,
+	    }
+	)
+	
+	return http_server
+	
+	
+async def main():
+	app = make_app()
+	try:
+		app.listen(int(listen_port), str(listen_addr))
+	except OSError as e:
+		if e.errno == errno.EADDRINUSE:
+			print("listen(): 'EADDRINUSE': " + str(e.args[1]))
+			sys.exit(1)
+		else:
+			raise
+			
+	#await asyncio.gather(PingLoop(), return_exceptions=True)
+	
+	#await asyncio.gather(PingLoop())
+	await asyncio.gather(FpingLoop())
+		
+
+
+# ------ Div -------------------------------
+
+
+def create_html_table():
+	html_tab = ""
+	html_tab += '<div id="top_header" class="top_header w3-black">'
+	html_tab += '<span class="w3-left tbars" style="text-align: left;">V'+VERSION+' by <a href="https://pss.zweiernet.ch" target="_blank">PSS</a><br><span style="font-size:1em;opacity:0.8;background-color:#4e4ec4;cursor:pointer;" onclick="document.getElementById(\'modal_01\').style.display=\'block\'">&nbsp;<span class="w3-text-white w3-monospace"><b>i</b></span> &#x2754; </span></span>'
+	html_tab += '<span class="w3-center top_head_tit">PingPanel<span class="top_head_tit_pfix"> ' + config["Main"]["TitleAdd"] + '</span></span>'
+	html_tab += '<span class="w3-right tbars"><span id="cur_actime" style="text-align: right;color: #22ff00;font-weight: bold;"> </span><br><button id="close_button" onclick="socket_close(1000)" class="sbutt">stop</button><br></span>'
+	html_tab += '</div>\n'
+	html_tab += '<div class="w3-container">'
+	for sect in config.sections():
+		if (sect == "Main"):
+			continue
+		html_tab += '<div class="w3-container" style="padding: 0;font-size:14px;">'
+		sect_x = sect.replace(' ','_')
+		clickId = 'accord_' + sect_x
+		html_tab += '<br><div onclick="Accordion(\'' + clickId + '\')" class="w3-container w3-teal" style="padding: 3px 10px;opacity: 0.9;margin-top: 5px;box-shadow: 0 2px 5px 0 #04040424,0 2px 10px 0 #040404c4;letter-spacing: 2px;cursor: pointer;">' + sect + '<span id="ux_' + sect_x + '" class="w3-card-2 w3-right"></span></div>\n'
+		html_tab += '<div id="' + clickId + '" class="w3-show">'
+		for key in config[sect]:
+			tkey = key.replace('.', '_')
+			html_tab += '<div class="w3-left w3-dark-grey w3-border w3-border-teal tooltip" style="text-align: center;margin: 1px; min-width: 13ch; padding: 4px min(3px, 1vw) !important;box-shadow:3px 2px 4px #0009 !important;">'
+			html_tab += '<span class="w3-card-2 tooltiptext">'+config[sect][key]+'<br><span id="ttip_' + tkey + '" class="ttip_small"></span></span>'
+			html_tab += '<span id="' + tkey + '" class="ip_num">' + key + '</span><br><span id="stat_' + tkey + '" class="w3-center"></span></div>'
+		html_tab += '</div>\n</div>\n'
+	html_tab += '</div>\n'
+
+	
+	return html_tab
+		
+		
+		
+
+html_html = '''
+<!doctype html>
+<html>
+    <header>
+    	<title>PingPanel %s</title>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<meta charset="UTF-8"> 
+        <script type="text/javascript">
+        	window.onload = start_websocket();
+        	function start_websocket() {
+            	var ws = new WebSocket("%s://%s/websocket");
+            	ws.onmessage = function (evt) {
+            		//let test = evt.data.replace(/'/g, '"');		// needed " by JSON.parse
+            		//var json_arr = JSON.parse(test);
+            		var json_arr = JSON.parse(evt.data);
+            		var cur_actime = "";
+            		
+            		// Colortable counters initialisation
+            		let M_red = new Map();
+            		let M_green = new Map();
+            		let M_yellow = new Map();
+            		for( let i = 0; i < json_arr.length; i++ ) {
+            			if ( M_red.has(json_arr[i].sect) ) {
+            				continue;
+            			} else {
+            				M_red.set(json_arr[i].sect, 0);
+            				M_green.set(json_arr[i].sect, 0);
+            				M_yellow.set(json_arr[i].sect, 0);
+            			}
+            		}
+
+            		// DOM Updates
+            		for(let i = 0; i < json_arr.length; i++) {
+            			var data_obj = json_arr[i];
+            			cur_actime = data_obj.actime;
+            	   		var cur_host = data_obj.host;
+            	   		var cur_status = data_obj.status;
+            	   		var cur_sect_ux = data_obj.sect;
+            	   		var cur_rtt = data_obj.rtt;
+            	   		cur_rtt = cur_rtt.replace(/ /g,' \/ ');
+            	   		var uc_cur_host = cur_host.replace(/\./g,'_');
+            	   		document.getElementById(uc_cur_host).textContent = cur_host;
+            	   		var stat_id = 'stat_' + uc_cur_host;
+            	   		var ttip_id = 'ttip_' + uc_cur_host;
+            	   		if (cur_status == 'OK') {
+            	   			M_green.set(cur_sect_ux, M_green.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/green_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else if (cur_status == 'FAILED') {
+            	   			M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/red_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else if (cur_status == 'NEWFAIL') {
+            	   			M_red.set(cur_sect_ux, M_red.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/red_anim.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		} else {
+            	   			M_yellow.set(cur_sect_ux, M_yellow.get(cur_sect_ux) + 1);
+            	   			document.getElementById(stat_id).innerHTML = '<img src="/img/yellow_on.gif">';
+            	   			document.getElementById(ttip_id).innerHTML = '<b>RTT</b>: '+cur_rtt;
+            	   		}
+            	   	} // End for
+            	   	document.getElementById('cur_actime').innerHTML = cur_actime;
+            	   	for (let [sect, val] of M_red) {
+            	   		document.getElementById(sect).innerHTML = '<span class="unreach_r"><span class="red_r">'+M_red.get(sect).toString().padStart(3, " ")+'</span> | <span class="yellow_r">'+M_yellow.get(sect).toString().padStart(3, " ")+'</span> | <span class="green_r">'+M_green.get(sect).toString().padStart(3, " ")+'</span></span>';
+            	   	}
+            	};
+            	
+            	ws.onclose = function(evt) {
+   					//alert("WebSocket closed.");
+   					document.getElementById('cur_actime').innerHTML = '<span style="color:red;">stopped</span>';
+   					document.getElementById('close_button').style.opacity = "0.3";
+   					//document.getElementById('modal_alert_text').style.display='block';
+   					//document.getElementById('modal_alert_text').innerHTML = 'WebSocket closed.';
+				};
+				ws.onerror = function(evt) {
+   					alert("WebSocket closed due to an error.");
+   					document.getElementById('cur_actime').innerHTML = '<span style="color:red;">Error</span>';
+   					document.getElementById('close_button').style.opacity = "0.3";
+				};
+				socket_close = (code) => {
+					document.getElementById('close_button').style.opacity = "0.3";
+					ws.close(code);
+				};
+            };
+            
+            function Accordion(id) {
+            	var x = document.getElementById(id);	//alert(x);   //.previousSibling.getElementsByTagName("*").item(0).innerHTML);
+				if (x.className.indexOf("w3-show") == -1) {
+					x.className = "w3-show";
+				} else { 
+					x.className = x.className.replace("w3-show", "w3-hide");
+				}
+			};
+        </script>
+        <script>
+        // modal handling
+		var modal = document.getElementById('modal_01');
+		// When the user clicks anywhere outside of the modal, close it
+		window.onclick = function(event) {
+			if (event.target == modal) {
+		    	modal.style.display = "none";
+			}
+		}
+        </script>
+        
+        <link rel="stylesheet" href="/css/w3.css">
+        <style>
+        	%s
+        </style>
+    </header>
+    <body>
+        <div id="main">%s</div>\n
+        
+        
+        <div id="modal_01" class="w3-modal modal_01" onclick="this.style.display='none'">
+        	<div class="w3-modal-content modal_01_text">
+    			<div class="w3-container">
+  					<h4 style="font-variant:small-caps;letter-spacing:3px;">PingPanel</h4>
+  					%s
+  				</div>
+  			</div>
+		</div>
+    </body>
+</html>
+'''
+
+html_css = '''
+body {
+    background: #333;
+    font-family: Verdana, Arial, sans-serif;
+    color: #ddd;
+    line-height: 110%;
+    font-size: 13px;
+}
+.red { color: red; }
+.green { color: #0f0; }
+.yellow { color: yellow; }
+.red_r { color: red; white-space: pre; text-align: right; font-family: monospace;}
+.green_r { color: #0f0; white-space: pre; text-align: right;; font-family: monospace;}
+.yellow_r { color: yellow; white-space: pre; text-align: right;; font-family: monospace;}
+.ip_num { font-size: 80%; }
+
+.top_header {
+  text-align: center;
+  padding: 5px 10px; height: 43px;
+  box-shadow: 0px 1px 9px 1px #c3d0cd78 !important;
+  position: sticky;
+  z-index: 100;
+  top: 0px;
+}
+
+.tbars {
+  color: #aaa;
+  font-size: 11px;
+  font-stretch: semi-expanded;
+}
+
+.unreach_r {
+  background-color:#313131;
+  padding-left:5px;
+  padding-right:5px;
+  letter-spacing: 0px;
+  border-radius: 2px;
+  font-weight: bold;
+  font-size: 12px;
+  font-family: Bitstream;
+}
+
+.modal_01 {
+  
+}
+.modal_01_text {
+  background-color: #000;
+  color: #f0f0ca;
+  line-height: 120%;
+  font-size: 1.1em;
+}
+
+.sbutt {
+  border: none;
+  display: inline-block;
+  vertical-align: middle;
+  overflow: hidden;
+  text-decoration: none;
+  text-align: center;
+  cursor: pointer;
+  white-space: nowrap;
+  border-radius: 4px;
+  padding: 0px 8px !important;
+  background-color: #9b9b9b !important;
+  color: #000 !important;
+}
+
+.top_head_tit {
+  font-size: 22px;
+  font-variant: small-caps;
+  letter-spacing: 4px;
+  top: 0.3em;
+  position: sticky;
+}
+.top_head_tit_pfix {
+  font-size: 0.5em;
+  font-variant: small-caps;
+  letter-spacing: 0px;
+}
+
+.ttip_small {
+  font-size: 64%;
+  text-align: center;
+  padding-top: 6px;
+}
+ 
+
+.tooltip {
+  position: relative;
+  display: inline-block;
+  border-bottom: 1px dotted black;
+}
+
+.tooltip .tooltiptext {
+  font-family: monospace;
+  visibility: hidden;
+  width: 150px;
+  background-color: black;
+  color: #fff;
+  text-align: center;
+  font-size: 15px;
+  border-radius: 6px;
+  padding: 5px 5px;
+  position: absolute;
+  z-index: 1;
+  top: 120%;
+  left: 50%;
+  margin-left: -60px;
+}
+
+.tooltip .tooltiptext::after {
+  content: "";
+  position: absolute;
+  bottom: 100%;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px;
+  border-style: solid;
+  border-color: transparent transparent black transparent;
+}
+
+.tooltip:hover .tooltiptext {
+  visibility: visible;
+}
+
+.sticky {
+  position: fixed;
+  top: 0;
+  width: 100%;
+}
+
+.sticky + .content {
+  padding-top: 50px;
+}
+
+'''
+
+def create_html_modal01():
+	html_modal = '''
+<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Info</p>
+ Config File: 
+ 	''' + konfig_file + '''<br>
+ Total number of Devices: _NUM_HOSTS_ <br>
+ LoopInterval: 
+ 	''' + str(loop_interval) + ''' s<br>
+ FlashThreshold: 
+ 	''' + str(config["Main"]["FlashThreshold"]) + ''' rounds<br>
+ fping: 
+	''' + fping_parameter[2].replace('--','') + '''<br>
+ fping: 
+	''' + fping_parameter[3].replace('--','') + ''' ms<br>
+ fping: 
+	''' + fping_parameter[4].replace('--','') + ''' ms<br>
+<br> 
+
+<p style="font-size:1.2em;font-variant:small-caps;letter-spacing:3px;font-style:italic;text-decoration:underline">Help</p>
+This PingPanel graphical interface consists of a header and the configured sections with the corresponding devices to be monitored.<br>
+
+Among other things, the header contains the current time (<span class="green">green</span>) of the last ping run 
+or the status "stopped" (<span class="red">red</span>) if the connection to the server has been lost or the "Stop" button below has been pressed.<br>
+Pressing the "Stop" button terminates the connection to the server and, if no other client is active, the ping rounds are suspended.<p>
+
+The blue-green <span class="w3-teal">section bars</span> contain the name of the section and consolidated information about the status of the devices in this section.<br>
+<u>The section bars can be expanded and collapsed by clicking or tapping on them.</u><br>
+Consolidated status information <img src="/img/stat-inf.gif"> is divided into <span class="red">failed</span>, 
+<span class="yellow">partially answered</span> and <span class="green">answered</span> ping requests.<br>
+
+Each monitored device displays its IP address and status. By hovering over or tapping on it the corresponding comment 
+is displayed and every ping RTT of each ping round is reported.<p>
+
+A <span class="red">flashing red</span> status light indicates that the device has freshly not responded since 'FlashThreshold' ping rounds.<br>
+A <span class="red">red</span> status light indicates that the device has not answered any of the three pings.<br>
+A <span class="yellow">yellow</span> status light indicates that the device has answered one of three pings.<br>
+A <span class="green">green</span> status light indicates that the device has answered to all three or at least two pings.<br>
+<p></p>
+<p> </p>
+
+
+
+
+
+
+
+
+	'''
+
+	html_modal = html_modal.replace('_NUM_HOSTS_', str(anz_hosts))
+
+	return html_modal
+
+
+
+# -------- MAIN ------------------------------
+if __name__ == "__main__":
+	
+	print("PingPanel v"+VERSION+" by PSS -- started at " + datetime.now().strftime("%F %H:%M:%S"))
+	# Commandline parsing
+	parser = argparse.ArgumentParser(description='PingPanel V'+VERSION+"\n2024 by sigi",
+                                   formatter_class=argparse.RawDescriptionHelpFormatter)
+	parser.add_argument('-c', help="Alternative configuration file (standard: config.cfg)", type=str, metavar='config-file')
+	parser.add_argument('-v', help="Verbose mode", action="store_true")
+	args = parser.parse_args()
+	if args.c:
+		konfig_file = args.c
+	print("Configfile: " + konfig_file)
+	
+	
+	# Config
+	read_config()
+	
+	
+	# create the app
+	asyncio.run(main())
+
+
+# ------
+
+

+ 13 - 0
contrib/PingPanel/opt/PingPanel/changelog

@@ -0,0 +1,13 @@
+v0.9.6 / 18.7.24, sigi:
+	- Corr: handling duplicate ping answers (ICMP replys).
+	- formatting status information fileds.
+	
+v0.9.7 / 30.7.24, sigi:
+	- Acceleration: relocation of json handling from frontend to backend.
+	- some CSS cosmetics.
+	
+v0.9.8 / 12.8.24, sigi:
+	- implementing secure HTTPS connections on both, webserver and websocket.
+	- Added HTTPS params in config.cfg.
+	- Changed: 'Title' param renamed to 'TitleAdd' in config.cfg as it is now an addendum to the title.
+	

+ 95 - 0
contrib/PingPanel/opt/PingPanel/config.cfg

@@ -0,0 +1,95 @@
+#-- Main Section:
+## ! Do not change nor delete the Main-Section name ([Main]) !
+## ! Do not use quotation marks, neither for keys nor for values !
+[Main]
+
+## The title addendum to PingPanel on top of the site and in the browser window title.
+## Pay attention to the length when using on smartphones.
+TitleAdd = 
+
+## Port the Webserver listen on:
+ListenPort = 8888
+
+## Address the Webserver is bound on:
+ListenAddr = 0.0.0.0
+
+## Using secure HTTPS conections:
+## Using HTTPS, comment out the following three parameters.
+## Of course, the file names are freely choosable here, relative to the current directory.
+#UseHTTPS = true
+#CertFile = server.crt
+#KeyFile = server.key
+
+
+## Location of the 'fping' command, without any quotation marks (defaults to: fping)
+fpingCommand = fping
+
+#-- fping related parameters (see https://fping.org/fping.8.html):
+## Fping-interval:
+##- from manpage - The minimum amount of time (in milliseconds) between sending a ping packet to any target
+##- from manpage - (default is 10, minimum is 1).
+## Increasing this value has a major impact on the runtime of a single ping round when a high number of hosts are pinged.
+## PingPanel default is 1.
+Fping-interval = 1
+
+## Fping-period:
+##- from manpage - This parameter sets the time in milliseconds that fping waits between successive
+##- from manpage - packets to an individual target. Default is 1000 and minimum is 10.
+## PingPanel default is 100.
+Fping-period = 100
+
+#-- End fping related params
+
+
+## LoopInterval:
+## The time that the program sleeps (in seconds) between ping runs (default is 3).
+## Since the ping is executed asynchronously, this LoopInterval is the waiting time between two completed
+## consecutive ping subprocesses. The time required by the ping subprocess itself depends on the number of hosts
+## and the fping parameters defined above. Tip: fiddle around with the shown fping command (argument PingPanel -v)
+## within the Linux 'time' command to find the optimal value for your needs.
+LoopInterval = 3
+
+## FlashThreshold:
+## Number of ping rounds for which a host does not respond until the status light changes from flashing red to permanently red.
+FlashThreshold = 3
+
+
+#-- User specified sections follow
+## The following sections consist of a section name followed by IP-Addresses with an associated comment.
+## The section name must be in square brackets and is case-sensitive. At least one section name must exist.
+## IP-Addresses can be in IPv4 or in IPv6 format and are delimited by an equal-sign (=) from comment.
+## Everything after the equal-sign is considered a comment. HTML-Tags are allowed in comment, but be careful
+## not to overload the page.
+## The comment can be omitted.
+## Leading and trailing whitespace is removed from comment and IP-Address by the program.
+[LOCAL]
+192.168.1.1		= One
+192.168.1.2		= Two
+192.168.1.3		= Three
+192.168.1.4		= Four
+192.168.1.7		= Seven
+192.168.1.8		= Eight
+192.168.1.9	= 
+192.168.1.10	= Router Number Ten
+192.168.1.11	= Switch Number Eleven
+
+
+[Zum Fröhlichen Zehnernetz]
+10.0.0.1    = happy 1
+10.0.0.2    = happy 2
+10.0.0.3    = happy 3
+10.0.0.4    = happy 4
+10.0.0.5    = happy 5
+10.0.1.100  = 
+
+
+[REMOTE]
+212.51.140.225			= <span style="color: yellow">ZweierNet.ch</span>
+2a02:168:6229::10		= <span style="color: blue">ZweierNet.ch IPv6</span>
+8.8.8.8					= The Quad Eight
+2001:4860:4860::8888 	= Quad Eight IPv6
+9.9.9.9					= The Nines
+2620:fe::9				= The Nines IPv6
+1.1.1.1					= The One and One and One and One
+2606:4700:4700::1111 	= The Ones IPv6
+

+ 6 - 0
contrib/PingPanel/opt/PingPanel/create_server-certificate.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+openssl req  -nodes -new -x509 -days 3650 -keyout server.key -out server.crt
+
+echo "Server certificate created: 'server.crt', Keyfile: 'server.key'"
+

+ 14 - 0
contrib/PingPanel/opt/PingPanel/etc.example/default/pingpanel

@@ -0,0 +1,14 @@
+# /etc/default/pingpanel
+
+# Defaults for pingpanel initscript. This file is sourced by /etc/init.d/pingpanel.
+# all vars used in /etc/init.d/pingpanel can be set here.
+#
+
+# Configfile (Standard: config.cfg)
+CONFIG=/opt/PingPanel/config.cfg
+
+# the path to logfile
+LOGFILE=/var/log/pingpanel.log
+
+# Options to pass to PingPanel.py
+#PINGPANEL_OPTS=""

+ 79 - 0
contrib/PingPanel/opt/PingPanel/etc.example/init.d/pingpanel

@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# /etc/init.d/pingpanel                  Startup script for the PingPanel process
+#
+### BEGIN INIT INFO
+# Provides:          pingpanel
+# Required-Start:    $remote_fs
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: PingPanel monitoring of network devices
+# Description:       PingPanel is a utility for monitoring network devices
+#                    and provides a web-based service, the panel, for
+#                                        displaying the captured statuses.
+### END INIT INFO
+
+. /lib/lsb/init-functions
+
+DAEMON=/opt/PingPanel/PingPanel.py
+CONFIG=/opt/PingPanel/config.cfg
+NAME=pingpanel
+DESC="PingPanel monitor service"
+LOGFILE=/var/log/pingpanel.log
+PINGPANEL_OPTS=
+
+set -e
+
+# Check if DAEMON binary exist
+[ -f $DAEMON ] || exit 0
+
+[ -f "/etc/default/$NAME" ] && . /etc/default/$NAME
+
+PINGPANEL_OPTS="-c $CONFIG $PINGPANEL_OPTS"
+
+case "$1" in
+  start)
+    log_daemon_msg "Starting $DESC" "$NAME"
+	
+	start_daemon /usr/bin/python3 -u $DAEMON $PINGPANEL_OPTS 1>>$LOGFILE 2>>$LOGFILE &
+
+    if pgrep -f $DAEMON
+    then
+      log_success_msg "$DAEMON running."
+    else
+      log_failure_msg "$DAEMON start FAILED!"
+    fi
+    ;;
+  stop)
+    log_daemon_msg "Stopping $DESC" "$NAME"
+
+    kill -9 $(pgrep -f $DAEMON)
+    if pgrep -f $DAEMON
+    then
+      log_end_msg 1
+    else
+      log_end_msg 0
+    fi
+    ;;
+  restart)
+    $0 stop
+    sleep 2
+    $0 start
+    ;;
+  status)
+    if pgrep -f $DAEMON
+	then
+      log_end_msg 0
+      exit 0
+    else
+      log_end_msg 1
+      exit 1
+    fi
+    ;;
+  *)
+    log_action_msg "Usage: /etc/init.d/$NAME {start|stop|restart|status}"
+    ;;
+esac
+
+exit 0

BIN
contrib/PingPanel/opt/PingPanel/favicon.ico


BIN
contrib/PingPanel/opt/PingPanel/green_on.gif


BIN
contrib/PingPanel/opt/PingPanel/red_anim.gif


BIN
contrib/PingPanel/opt/PingPanel/red_on.gif


+ 20 - 0
contrib/PingPanel/opt/PingPanel/server.crt

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgIUAjtvqb5ffp4CZ96HrvIu+7rCY1wwDQYJKoZIhvcNAQEL
+BQAwMDELMAkGA1UEBhMCY2gxEzARBgNVBAgMClNvbWUtU3RhdGUxDDAKBgNVBAoM
+A1BTUzAeFw0yNDAyMTMxNzQzMDFaFw0zNDAyMTAxNzQzMDFaMDAxCzAJBgNVBAYT
+AmNoMRMwEQYDVQQIDApTb21lLVN0YXRlMQwwCgYDVQQKDANQU1MwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDmRv9bQ9mGOrgxxXX/5ilhmZu0dCsn7j43
+Vv2dQ1ZTnW6k3DZzFk693xFbtJ+2EJkHhUhqS0IQbZI9fhnKR6yfMUJ7uip1ifdd
+bnrMH6H+xRHjbf+Gjmh6N2x5q0x0woCUo/gcvbQuJdY8EoC57TuuBNx83/G3pIi6
+Pute7K6H4S9bnKkB/Xf2UohxNCmU5fUMikgXoRMjqt9xV8gRV62CT+MFJ4z8am0L
+6f5okMsx17zJCGECcVVMMPEQdhv1s3cjvVJqcPEoAdLAPZPIy9JPk28dF1evjw+w
+IBnITNYY1z/k52sizKMWC/lu0d51TMGTQUvcs/X2OH3Yl+WUNQcRAgMBAAGjUzBR
+MB0GA1UdDgQWBBTHgEb2mzUty1qi2SF1oguPF++MxTAfBgNVHSMEGDAWgBTHgEb2
+mzUty1qi2SF1oguPF++MxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
+A4IBAQDlI21Lsa+Iz4KVGCoSSC39U2a27c549vQTaY9BsRhEpNh+U515CIHCLBoI
+ADK9Jh76DF/ie6BoHRPaCkoK3CSBOLOr4uB8jUSZrOT67RmnerZIcQQYboquNJFC
+nV97RpL2ri0UdzoocvF6GHms+kNccq81D1p5SmMJSdEj5YVtzQMzpLzmOaWIqN2J
+ld0TGvce6rId9JxIXUOcahgod0GWYsMZdgCs8i+SFgLm8BQPmdk/JybTiWRjcW+c
+q3xL2mGQDoHd01m1YRKKKgHjguAAVkrDSRc3ulGPjLR1w222ugtHIfB6gvYRJ8WI
+DgbnzF+V8eoe6w7W1bqjZ8a3D6XW
+-----END CERTIFICATE-----

+ 28 - 0
contrib/PingPanel/opt/PingPanel/server.key

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDmRv9bQ9mGOrgx
+xXX/5ilhmZu0dCsn7j43Vv2dQ1ZTnW6k3DZzFk693xFbtJ+2EJkHhUhqS0IQbZI9
+fhnKR6yfMUJ7uip1ifddbnrMH6H+xRHjbf+Gjmh6N2x5q0x0woCUo/gcvbQuJdY8
+EoC57TuuBNx83/G3pIi6Pute7K6H4S9bnKkB/Xf2UohxNCmU5fUMikgXoRMjqt9x
+V8gRV62CT+MFJ4z8am0L6f5okMsx17zJCGECcVVMMPEQdhv1s3cjvVJqcPEoAdLA
+PZPIy9JPk28dF1evjw+wIBnITNYY1z/k52sizKMWC/lu0d51TMGTQUvcs/X2OH3Y
+l+WUNQcRAgMBAAECggEBAIWTqYzHTucOKMq2QDywxlBXSnri6CXBjVWMoJEY/nqC
+WCxJkUwxJHv/aZJItFxoRFyYc+k0vp3I8Yu6Gju8V+YALRCYbQjBfzwzWggOUWn3
+5uiGZjMnaHN8su/g7fjM2sleVA5X/KzIRf2SvhkcBAJtz3igbxaX2rgux1nI8XTf
+kVXVwbYZEj0bpg6JoEa8mQ2NxxykBAlTEbKm/vY9PMKtBbRUvmkThRBPRXk0C/zz
+nOvqW3qyuYlZ3rC0UXJ/YEo9QiNOlbYg2qiizAJ3zSDD7K4rgXXFFI/wYP2PiHyT
+nQ2zRiuG4e9ey2xS5eAKc827BShhLfO2WmSRZRDyAAECgYEA+JtkPGWBfcwM/wN0
+BUUfLAJWgOjdKKWMpmHR7LKsPQfqyEFZLkTPTzi7y43PPIoztpkmXDMTrj/+WjPS
+lOizzvhh+N4+ov+HBqOinFyQ45p7QCvz3ft1x9173aFlmjjKQNjWg/jexMqarYHU
+RsiThQPzIsDMHXhrDWU/cbnVEXECgYEA7SAQnamqCRmV5IL6cHv4/tfyj7tnd4wv
+R6zg48re0CfMDS4URwc3QA/Pqwo1BiLnFQ8UZKsRcu0JYHzVgZ4f2odjBiZfmtcW
+sp7HAKc/kXN4oazhpXBkM2zl2El8T55aZkHUAGJMl64hmduMH4Ut5cVLBDzVKc4m
+jnAIqDNuf6ECgYEArlkD5dtmAdv9bUZ1slB0eP+2vLcSirP3PKQzfaUcZ7zKqeAy
+c+Fr5eoqwalVIebmN3OWVGi5r6VPcuEPGMFNgKPuyYLLOLKtdjmCC9hbAFPRhgKN
+ByuXwTAR8y+COrMDHJE1d94sFKTl7zKytWVrF4jalY/SVgSiRwWvhclGpPECgYEA
+ncR7yaqc6zkOtd8/aZ/SZxye0mrJyIyI2JN5fZX48V3JMeu6qhdu9zyN2ysLZG3M
+egJyexgmn+R+HKhyFa9zWh5CEFFvwsHg5C8oEJM2hDvjww4Xg89nm5+UvXcHMqIV
+W3vo5eiBNVoyGQAuFrqxcvL1mngvC4WRuato1yCBIIECgYEApsoZlRbXM4qMxw8Y
+OFireME2BrSORuguP/AVO/mghE4kOzi5JlJW44kt4fEFqDKaEmnL81C4El4mVWG9
+RozIXpXmZycsxfg8mihtZbgKOkicucup2/jVhcGup/Z0ZJemwBw/M7jJOFrfhJwJ
+U5zQfe8TG0JOLcK41N2c2lXn6Xo=
+-----END PRIVATE KEY-----

BIN
contrib/PingPanel/opt/PingPanel/stat-inf.gif


+ 236 - 0
contrib/PingPanel/opt/PingPanel/w3.css

@@ -0,0 +1,236 @@
+/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
+html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
+/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
+html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
+article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
+audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
+audio:not([controls]){display:none;height:0}[hidden],template{display:none}
+a{background-color:transparent}a:active,a:hover{outline-width:0}
+abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
+b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
+small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
+code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
+button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
+button,input{overflow:visible}button,select{text-transform:none}
+button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
+button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
+button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
+fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
+legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
+[type=checkbox],[type=radio]{padding:0}
+[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
+[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
+[type=search]::-webkit-search-decoration{-webkit-appearance:none}
+::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
+/* End extract */
+html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
+h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
+.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
+h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
+hr{border:0;border-top:1px solid #eee;margin:20px 0}
+.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
+.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
+.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
+.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
+.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
+.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
+.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
+.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
+.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
+.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}   
+.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
+.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
+.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
+.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
+.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
+.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
+.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
+.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
+.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
+.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
+.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
+.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
+.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
+.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
+.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
+.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
+.w3-main,#main{transition:margin-left .4s}
+.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
+.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
+.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
+.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
+.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
+.w3-bar .w3-button{white-space:normal}
+.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
+.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
+.w3-responsive{display:block;overflow-x:auto}
+.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
+.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
+.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
+.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
+.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
+.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
+@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
+.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
+.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
+@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
+.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
+.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
+.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
+.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
+.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
+.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
+.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
+@media (max-width:1205px){.w3-auto{max-width:95%}}
+@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
+.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	
+.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
+.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
+@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
+@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
+@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
+@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
+.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
+.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
+.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
+.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
+.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
+.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
+.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
+.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
+.w3-display-position{position:absolute}
+.w3-circle{border-radius:50%}
+.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
+.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
+.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
+.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
+.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
+.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
+.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
+.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
+.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
+.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
+.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
+.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
+.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
+.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
+.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
+.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
+.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
+.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
+.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
+.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
+.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
+.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
+.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
+.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
+.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
+.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
+.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
+.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
+.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
+.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
+.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
+.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
+.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
+.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
+.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
+.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
+.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
+.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
+.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
+.w3-left{float:left!important}.w3-right{float:right!important}
+.w3-button:hover{color:#000!important;background-color:#ccc!important}
+.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
+.w3-hover-none:hover{box-shadow:none!important}
+/* Colors */
+.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
+.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
+.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
+.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
+.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
+.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
+.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
+.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
+.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
+.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
+.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
+.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
+.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
+.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
+.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
+.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
+.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
+.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
+.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
+.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
+.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
+.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
+.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
+.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
+.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
+.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
+.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
+.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
+.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
+.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
+.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
+.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
+.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
+.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
+.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
+.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
+.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
+.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
+.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
+.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
+.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
+.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
+.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
+.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
+.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
+.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
+.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
+.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
+.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
+.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
+.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
+.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
+.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
+.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
+.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
+.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
+.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
+.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
+.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
+.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
+.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
+.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
+.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
+.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
+.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
+.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
+.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
+.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
+.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
+.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
+.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
+.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
+.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
+.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
+.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
+.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
+.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
+.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
+.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
+.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
+.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
+.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
+.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
+.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
+

BIN
contrib/PingPanel/opt/PingPanel/yellow_on.gif


BIN
contrib/pingpanel_0.9.1_all.deb


BIN
contrib/pingpanel_0.9.2.tar.gz


BIN
contrib/pingpanel_0.9.2_all.deb


BIN
contrib/pingpanel_0.9.3_all.deb


BIN
contrib/pingpanel_0.9.4_all.deb


BIN
contrib/pingpanel_0.9.5.tar.gz


BIN
contrib/pingpanel_0.9.5_all.deb


BIN
contrib/pingpanel_0.9.6.tar.gz


BIN
contrib/pingpanel_0.9.6_all.deb


BIN
contrib/pingpanel_0.9.7.tar.gz


BIN
contrib/pingpanel_0.9.7_all.deb


BIN
contrib/pingpanel_0.9.8.tar.gz


BIN
contrib/pingpanel_0.9.8_all.deb


+ 4 - 0
contrib/si_README.txt

@@ -0,0 +1,4 @@
+- Debian Paket erstellt in diesem Verzeichnis mit: "dpkg-deb --build PingPanel ."
+- Versionsnummer wird aus control-File geholt.
+
+

+ 6 - 0
create_server-certificate.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+openssl req  -nodes -new -x509 -days 3650 -keyout server.key -out server.crt
+
+echo "Server certificate created: 'server.crt', Keyfile: 'server.key'"
+

+ 14 - 0
etc.example/default/pingpanel

@@ -0,0 +1,14 @@
+# /etc/default/pingpanel
+
+# Defaults for pingpanel initscript. This file is sourced by /etc/init.d/pingpanel.
+# all vars used in /etc/init.d/pingpanel can be set here.
+#
+
+# Configfile (Standard: config.cfg)
+CONFIG=/opt/PingPanel/config.cfg
+
+# the path to logfile
+LOGFILE=/var/log/pingpanel.log
+
+# Options to pass to PingPanel.py
+#PINGPANEL_OPTS=""

+ 79 - 0
etc.example/init.d/pingpanel

@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# /etc/init.d/pingpanel                  Startup script for the PingPanel process
+#
+### BEGIN INIT INFO
+# Provides:          pingpanel
+# Required-Start:    $remote_fs
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: PingPanel monitoring of network devices
+# Description:       PingPanel is a utility for monitoring network devices
+#                    and provides a web-based service, the panel, for
+#                                        displaying the captured statuses.
+### END INIT INFO
+
+. /lib/lsb/init-functions
+
+DAEMON=/opt/PingPanel/PingPanel.py
+CONFIG=/opt/PingPanel/config.cfg
+NAME=pingpanel
+DESC="PingPanel monitor service"
+LOGFILE=/var/log/pingpanel.log
+PINGPANEL_OPTS=
+
+set -e
+
+# Check if DAEMON binary exist
+[ -f $DAEMON ] || exit 0
+
+[ -f "/etc/default/$NAME" ] && . /etc/default/$NAME
+
+PINGPANEL_OPTS="-c $CONFIG $PINGPANEL_OPTS"
+
+case "$1" in
+  start)
+    log_daemon_msg "Starting $DESC" "$NAME"
+	
+	start_daemon /usr/bin/python3 -u $DAEMON $PINGPANEL_OPTS 1>>$LOGFILE 2>>$LOGFILE &
+
+    if pgrep -f $DAEMON
+    then
+      log_success_msg "$DAEMON running."
+    else
+      log_failure_msg "$DAEMON start FAILED!"
+    fi
+    ;;
+  stop)
+    log_daemon_msg "Stopping $DESC" "$NAME"
+
+    kill -9 $(pgrep -f $DAEMON)
+    if pgrep -f $DAEMON
+    then
+      log_end_msg 1
+    else
+      log_end_msg 0
+    fi
+    ;;
+  restart)
+    $0 stop
+    sleep 2
+    $0 start
+    ;;
+  status)
+    if pgrep -f $DAEMON
+	then
+      log_end_msg 0
+      exit 0
+    else
+      log_end_msg 1
+      exit 1
+    fi
+    ;;
+  *)
+    log_action_msg "Usage: /etc/init.d/$NAME {start|stop|restart|status}"
+    ;;
+esac
+
+exit 0

BIN
favicon.ico


BIN
green_on.gif


BIN
red_anim.gif


BIN
red_on.gif


+ 20 - 0
server.crt

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgIUAjtvqb5ffp4CZ96HrvIu+7rCY1wwDQYJKoZIhvcNAQEL
+BQAwMDELMAkGA1UEBhMCY2gxEzARBgNVBAgMClNvbWUtU3RhdGUxDDAKBgNVBAoM
+A1BTUzAeFw0yNDAyMTMxNzQzMDFaFw0zNDAyMTAxNzQzMDFaMDAxCzAJBgNVBAYT
+AmNoMRMwEQYDVQQIDApTb21lLVN0YXRlMQwwCgYDVQQKDANQU1MwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDmRv9bQ9mGOrgxxXX/5ilhmZu0dCsn7j43
+Vv2dQ1ZTnW6k3DZzFk693xFbtJ+2EJkHhUhqS0IQbZI9fhnKR6yfMUJ7uip1ifdd
+bnrMH6H+xRHjbf+Gjmh6N2x5q0x0woCUo/gcvbQuJdY8EoC57TuuBNx83/G3pIi6
+Pute7K6H4S9bnKkB/Xf2UohxNCmU5fUMikgXoRMjqt9xV8gRV62CT+MFJ4z8am0L
+6f5okMsx17zJCGECcVVMMPEQdhv1s3cjvVJqcPEoAdLAPZPIy9JPk28dF1evjw+w
+IBnITNYY1z/k52sizKMWC/lu0d51TMGTQUvcs/X2OH3Yl+WUNQcRAgMBAAGjUzBR
+MB0GA1UdDgQWBBTHgEb2mzUty1qi2SF1oguPF++MxTAfBgNVHSMEGDAWgBTHgEb2
+mzUty1qi2SF1oguPF++MxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
+A4IBAQDlI21Lsa+Iz4KVGCoSSC39U2a27c549vQTaY9BsRhEpNh+U515CIHCLBoI
+ADK9Jh76DF/ie6BoHRPaCkoK3CSBOLOr4uB8jUSZrOT67RmnerZIcQQYboquNJFC
+nV97RpL2ri0UdzoocvF6GHms+kNccq81D1p5SmMJSdEj5YVtzQMzpLzmOaWIqN2J
+ld0TGvce6rId9JxIXUOcahgod0GWYsMZdgCs8i+SFgLm8BQPmdk/JybTiWRjcW+c
+q3xL2mGQDoHd01m1YRKKKgHjguAAVkrDSRc3ulGPjLR1w222ugtHIfB6gvYRJ8WI
+DgbnzF+V8eoe6w7W1bqjZ8a3D6XW
+-----END CERTIFICATE-----

+ 28 - 0
server.key

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDmRv9bQ9mGOrgx
+xXX/5ilhmZu0dCsn7j43Vv2dQ1ZTnW6k3DZzFk693xFbtJ+2EJkHhUhqS0IQbZI9
+fhnKR6yfMUJ7uip1ifddbnrMH6H+xRHjbf+Gjmh6N2x5q0x0woCUo/gcvbQuJdY8
+EoC57TuuBNx83/G3pIi6Pute7K6H4S9bnKkB/Xf2UohxNCmU5fUMikgXoRMjqt9x
+V8gRV62CT+MFJ4z8am0L6f5okMsx17zJCGECcVVMMPEQdhv1s3cjvVJqcPEoAdLA
+PZPIy9JPk28dF1evjw+wIBnITNYY1z/k52sizKMWC/lu0d51TMGTQUvcs/X2OH3Y
+l+WUNQcRAgMBAAECggEBAIWTqYzHTucOKMq2QDywxlBXSnri6CXBjVWMoJEY/nqC
+WCxJkUwxJHv/aZJItFxoRFyYc+k0vp3I8Yu6Gju8V+YALRCYbQjBfzwzWggOUWn3
+5uiGZjMnaHN8su/g7fjM2sleVA5X/KzIRf2SvhkcBAJtz3igbxaX2rgux1nI8XTf
+kVXVwbYZEj0bpg6JoEa8mQ2NxxykBAlTEbKm/vY9PMKtBbRUvmkThRBPRXk0C/zz
+nOvqW3qyuYlZ3rC0UXJ/YEo9QiNOlbYg2qiizAJ3zSDD7K4rgXXFFI/wYP2PiHyT
+nQ2zRiuG4e9ey2xS5eAKc827BShhLfO2WmSRZRDyAAECgYEA+JtkPGWBfcwM/wN0
+BUUfLAJWgOjdKKWMpmHR7LKsPQfqyEFZLkTPTzi7y43PPIoztpkmXDMTrj/+WjPS
+lOizzvhh+N4+ov+HBqOinFyQ45p7QCvz3ft1x9173aFlmjjKQNjWg/jexMqarYHU
+RsiThQPzIsDMHXhrDWU/cbnVEXECgYEA7SAQnamqCRmV5IL6cHv4/tfyj7tnd4wv
+R6zg48re0CfMDS4URwc3QA/Pqwo1BiLnFQ8UZKsRcu0JYHzVgZ4f2odjBiZfmtcW
+sp7HAKc/kXN4oazhpXBkM2zl2El8T55aZkHUAGJMl64hmduMH4Ut5cVLBDzVKc4m
+jnAIqDNuf6ECgYEArlkD5dtmAdv9bUZ1slB0eP+2vLcSirP3PKQzfaUcZ7zKqeAy
+c+Fr5eoqwalVIebmN3OWVGi5r6VPcuEPGMFNgKPuyYLLOLKtdjmCC9hbAFPRhgKN
+ByuXwTAR8y+COrMDHJE1d94sFKTl7zKytWVrF4jalY/SVgSiRwWvhclGpPECgYEA
+ncR7yaqc6zkOtd8/aZ/SZxye0mrJyIyI2JN5fZX48V3JMeu6qhdu9zyN2ysLZG3M
+egJyexgmn+R+HKhyFa9zWh5CEFFvwsHg5C8oEJM2hDvjww4Xg89nm5+UvXcHMqIV
+W3vo5eiBNVoyGQAuFrqxcvL1mngvC4WRuato1yCBIIECgYEApsoZlRbXM4qMxw8Y
+OFireME2BrSORuguP/AVO/mghE4kOzi5JlJW44kt4fEFqDKaEmnL81C4El4mVWG9
+RozIXpXmZycsxfg8mihtZbgKOkicucup2/jVhcGup/Z0ZJemwBw/M7jJOFrfhJwJ
+U5zQfe8TG0JOLcK41N2c2lXn6Xo=
+-----END PRIVATE KEY-----

BIN
stat-inf.gif


+ 236 - 0
w3.css

@@ -0,0 +1,236 @@
+/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
+html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
+/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
+html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
+article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
+audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
+audio:not([controls]){display:none;height:0}[hidden],template{display:none}
+a{background-color:transparent}a:active,a:hover{outline-width:0}
+abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
+b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
+small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
+code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
+button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
+button,input{overflow:visible}button,select{text-transform:none}
+button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
+button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
+button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
+fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
+legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
+[type=checkbox],[type=radio]{padding:0}
+[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
+[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
+[type=search]::-webkit-search-decoration{-webkit-appearance:none}
+::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
+/* End extract */
+html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
+h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
+.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
+h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
+hr{border:0;border-top:1px solid #eee;margin:20px 0}
+.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
+.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
+.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
+.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
+.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
+.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
+.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
+.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
+.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
+.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}   
+.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
+.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
+.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
+.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
+.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
+.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
+.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
+.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
+.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
+.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
+.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
+.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
+.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
+.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
+.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
+.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
+.w3-main,#main{transition:margin-left .4s}
+.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
+.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
+.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
+.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
+.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
+.w3-bar .w3-button{white-space:normal}
+.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
+.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
+.w3-responsive{display:block;overflow-x:auto}
+.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
+.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
+.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
+.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
+.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
+.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
+@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
+.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
+.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
+@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
+.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
+.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
+.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
+.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
+.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
+.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
+.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
+@media (max-width:1205px){.w3-auto{max-width:95%}}
+@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
+.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	
+.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
+.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
+@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
+@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
+@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
+@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
+.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
+.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
+.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
+.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
+.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
+.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
+.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
+.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
+.w3-display-position{position:absolute}
+.w3-circle{border-radius:50%}
+.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
+.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
+.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
+.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
+.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
+.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
+.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
+.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
+.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
+.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
+.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
+.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
+.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
+.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
+.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
+.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
+.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
+.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
+.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
+.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
+.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
+.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
+.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
+.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
+.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
+.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
+.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
+.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
+.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
+.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
+.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
+.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
+.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
+.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
+.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
+.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
+.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
+.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
+.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
+.w3-left{float:left!important}.w3-right{float:right!important}
+.w3-button:hover{color:#000!important;background-color:#ccc!important}
+.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
+.w3-hover-none:hover{box-shadow:none!important}
+/* Colors */
+.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
+.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
+.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
+.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
+.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
+.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
+.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
+.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
+.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
+.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
+.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
+.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
+.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
+.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
+.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
+.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
+.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
+.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
+.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
+.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
+.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
+.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
+.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
+.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
+.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
+.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
+.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
+.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
+.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
+.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
+.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
+.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
+.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
+.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
+.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
+.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
+.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
+.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
+.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
+.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
+.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
+.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
+.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
+.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
+.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
+.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
+.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
+.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
+.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
+.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
+.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
+.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
+.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
+.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
+.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
+.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
+.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
+.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
+.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
+.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
+.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
+.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
+.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
+.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
+.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
+.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
+.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
+.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
+.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
+.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
+.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
+.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
+.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
+.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
+.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
+.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
+.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
+.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
+.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
+.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
+.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
+.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
+.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
+.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
+

BIN
yellow_on.gif