├── img ├── splash.png ├── favicon.ico ├── favicon.png └── logo-plexDash.png ├── main_files ├── modules │ ├── config │ │ └── ping_hosts │ ├── cpu-load.html │ ├── shell_files │ │ ├── cpu_info.sh │ │ ├── number_of_cpu_cores.sh │ │ ├── memory_info.sh │ │ ├── logged_in_users.sh │ │ ├── bandwidth.sh │ │ ├── disk_partitions.sh │ │ ├── memcached.sh │ │ ├── recent_account_logins.sh │ │ ├── io_stats.sh │ │ ├── internet_speed.sh │ │ ├── common_applications.sh │ │ ├── network_connections.sh │ │ ├── ram_intensive_processes.sh │ │ ├── cpu_intensive_processes.sh │ │ ├── load_avg.sh │ │ ├── swap.sh │ │ ├── arp_cache.sh │ │ ├── cron_history.sh │ │ ├── user_accounts.sh │ │ ├── current_ram.sh │ │ ├── redis.sh │ │ ├── ip_addresses.sh │ │ ├── general_info.sh │ │ ├── ping.sh │ │ ├── pm2.sh │ │ ├── cpu_utilization.sh │ │ ├── download_transfer_rate.sh │ │ ├── upload_transfer_rate.sh │ │ └── scheduled_crons.sh │ ├── download-transfer-rate.html │ ├── upload-transfer-rate.html │ ├── cpu-utilization-chart.html │ ├── ram-chart.html │ ├── disk-space.html │ └── python_files │ │ └── speedtest_cli.py ├── sections │ ├── applications.html │ ├── accounts.html │ ├── basic-info.html │ ├── system-status.html │ └── network.html ├── app │ ├── loading.html │ ├── ui-elements │ │ ├── last-update.html │ │ └── top-bar.html │ ├── progress-bar-plugin.html │ ├── navbar.html │ ├── base-plugin.html │ ├── line-chart-plugin.html │ ├── key-value-list-plugin.html │ ├── multi-line-chart-plugin.html │ └── table-data-plugin.html ├── ping-speeds.html ├── index.php └── index.js ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── README.md ├── css ├── login.css ├── css.css ├── plexDash.css ├── main.css └── font-awesome.min.css ├── index.php ├── config └── login.php └── js ├── smoothie.min.js ├── modules.js ├── plexDash.js └── bootstrap.min.js /img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/img/splash.png -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/img/favicon.ico -------------------------------------------------------------------------------- /img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/img/favicon.png -------------------------------------------------------------------------------- /main_files/modules/config/ping_hosts: -------------------------------------------------------------------------------- 1 | google.com 2 | yahoo.com 3 | twitter.com 4 | -------------------------------------------------------------------------------- /fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /img/logo-plexDash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/img/logo-plexDash.png -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxmi/plexDash/HEAD/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /main_files/sections/applications.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /main_files/app/loading.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Loading... 4 |
5 | -------------------------------------------------------------------------------- /main_files/sections/accounts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /main_files/ping-speeds.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /main_files/modules/cpu-load.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/cpu_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(/usr/bin/lscpu \ 4 | | /usr/bin/awk -F: '{print "\""$1"\": \""$2"\"," } '\ 5 | ) 6 | 7 | echo "{" ${result%?} "}" -------------------------------------------------------------------------------- /main_files/app/ui-elements/last-update.html: -------------------------------------------------------------------------------- 1 | Loading... 2 | 3 | {{ timestamp | date:'hh:mm:ss a' }} 4 | -------------------------------------------------------------------------------- /main_files/modules/download-transfer-rate.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/number_of_cpu_cores.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | numberOfCores=$(/bin/grep -c 'model name' /proc/cpuinfo) 4 | 5 | if [length($numberOfCores)]; then 6 | echo "cannnot be found"; 7 | fi -------------------------------------------------------------------------------- /main_files/app/progress-bar-plugin.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /main_files/modules/shell_files/memory_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /bin/cat /proc/meminfo \ 3 | | /usr/bin/awk -F: 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 "\"," } END {print "}"}' \ 4 | | /bin/sed 'N;$s/,\n/\n/;P;D' 5 | -------------------------------------------------------------------------------- /main_files/modules/upload-transfer-rate.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /main_files/sections/basic-info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/logged_in_users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(COLUMNS=300 /usr/bin/w -h | /usr/bin/awk '{print "{\"user\": \"" $1 "\", \"from\": \"" $3 "\", \"when\": \"" $4 "\"},"}') 3 | 4 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/app/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | -------------------------------------------------------------------------------- /main_files/app/base-plugin.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 | 9 |
10 |
-------------------------------------------------------------------------------- /main_files/modules/shell_files/bandwidth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /bin/cat /proc/net/dev \ 3 | | awk 'BEGIN {print "["} NR>2 {print "{ \"interface\": \"" $1 "\"," \ 4 | " \"tx\": " $2 "," \ 5 | " \"rx\": " $10 " }," } END {print "]"}' \ 6 | | /bin/sed 'N;$s/,\n/\n/;P;D' 7 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/disk_partitions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/df -Ph | awk 'NR>1 {print "{\"file_system\": \"" $1 "\", \"size\": \"" $2 "\", \"used\": \"" $3 "\", \"avail\": \"" $4 "\", \"used%\": \"" $5 "\", \"mounted\": \"" $6 "\"},"}') 3 | 4 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/modules/shell_files/memcached.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "stats" \ 3 | | /bin/nc -w 1 127.0.0.1 11211 \ 4 | | /bin/grep 'bytes' \ 5 | | /usr/bin/awk 'BEGIN {print "{"} {print "\"" $2 "\": " $3 } END {print "}"}' \ 6 | | /usr/bin/tr '\r' ',' \ 7 | | /bin/sed 'N;$s/,\n/\n/;P;D' -------------------------------------------------------------------------------- /main_files/sections/system-status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /main_files/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ heading }} 5 | 6 | {{ info }} 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/recent_account_logins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/usr/bin/lastlog -t 365 \ 3 | | /usr/bin/awk 'NR>1 {\ 4 | print "{ \ 5 | \"user\": \"" $1 "\", \ 6 | \"ip\": \"" $3 "\","" \ 7 | \"date\": \"" $5" "$6" "$7" "$8" "$9 "\"}," 8 | }' 9 | ) 10 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/modules/shell_files/io_stats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/cat /proc/diskstats | /usr/bin/awk \ 3 | '{ if($4==0 && $8==0 && $12==0 && $13==0) next } \ 4 | {print "{ \"device\": \"" $3 "\", \"reads\": \""$4"\", \"writes\": \"" $8 "\", \"in_progress\": \"" $12 "\", \"time_in_io\": \"" $13 "\"},"}' 5 | ) 6 | 7 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/modules/shell_files/internet_speed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPTPATH=`dirname $(readlink -f $0)` 4 | SPEED_TEST_SCRIPT=$SCRIPTPATH"/../python_files/speedtest_cli.py" 5 | 6 | $SPEED_TEST_SCRIPT \ 7 | | grep 'Upload\|Download' \ 8 | | awk 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 " " $3 "\"," } END {print "}"}' \ 9 | | /bin/sed 'N;$s/",/"/;P;D' -------------------------------------------------------------------------------- /main_files/modules/cpu-utilization-chart.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /main_files/sections/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /main_files/modules/ram-chart.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/common_applications.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(whereis php node mysql mongo vim python ruby java apache2 nginx openssl vsftpd make \ 3 | | awk -F: '{if(length($2)==0) { installed="false"; } else { installed="true"; } \ 4 | print \ 5 | "{ \ 6 | \"binary\": \""$1"\", \ 7 | \"location\": \""$2"\", \ 8 | \"installed\": "installed" \ 9 | },"}') 10 | 11 | echo "[" ${result%?} "]" -------------------------------------------------------------------------------- /main_files/modules/shell_files/network_connections.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | netstatCmd=`which netstat` 3 | awkCmd=`which awk` 4 | sortCmd=`which sort` 5 | uniqCmd=`which uniq` 6 | sedCmd=`which sed` 7 | 8 | $netstatCmd -ntu \ 9 | | $awkCmd 'NR>2 {print $5}' \ 10 | | $sortCmd \ 11 | | $uniqCmd -c \ 12 | | $awkCmd 'BEGIN {print "["} {print "{ \"connections\": " $1 ", \"address\": \"" $2 "\" }," } END {print "]"}' \ 13 | | $sedCmd 'N;$s/},/}/;P;D' 14 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/ram_intensive_processes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/ps axo pid,user,pmem,rss,vsz,comm --sort -pmem,-rss,-vsz \ 3 | | head -n 15 \ 4 | | /usr/bin/awk 'NR>1 {print "{ \"pid\": " $1 \ 5 | ", \"user\": \"" $2 \ 6 | "\", \"mem%\": " $3 \ 7 | ", \"rss\": " $4 \ 8 | ", \"vsz\": " $5 \ 9 | ", \"cmd\": \"" $6 \ 10 | "\"},"}') 11 | 12 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/modules/shell_files/cpu_intensive_processes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(/bin/ps axo pid,user,pcpu,rss,vsz,comm --sort -pcpu,-rss,-vsz \ 4 | | head -n 15 \ 5 | | /usr/bin/awk 'BEGIN{OFS=":"} NR>1 {print "{ \"pid\": " $1 \ 6 | ", \"user\": \"" $2 "\"" \ 7 | ", \"cpu%\": " $3 \ 8 | ", \"rss\": " $4 \ 9 | ", \"vsz\": " $5 \ 10 | ", \"cmd\": \"" $6 "\"" "},"\ 11 | }') 12 | 13 | echo "[" ${result%?} "]" -------------------------------------------------------------------------------- /main_files/modules/shell_files/load_avg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grepCmd=`which grep` 4 | awkCmd=`which awk` 5 | catCmd=`which cat` 6 | 7 | numberOfCores=$($grepCmd -c 'processor' /proc/cpuinfo) 8 | 9 | if [ $numberOfCores -eq 0 ]; then 10 | numberOfCores=1 11 | fi 12 | 13 | result=$($catCmd /proc/loadavg | $awkCmd '{print "{ \"1_min_avg\": " ($1*100)/'$numberOfCores' ", \"5_min_avg\": " ($2*100)/'$numberOfCores' ", \"15_min_avg\": " ($3*100)/'$numberOfCores' "}," }') 14 | 15 | echo ${result%?} 16 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/swap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | catCmd=`which cat`; 4 | wcCmd=`which wc`; 5 | awkCmd=`which awk` 6 | 7 | swapLineCount=$($catCmd /proc/swaps | $wcCmd -l) 8 | 9 | if [ "$swapLineCount" -gt 1 ]; then 10 | 11 | result=$($catCmd /proc/swaps \ 12 | | $awkCmd 'NR>1 {print "{ \"filename\": \"" $1"\", \"type\": \""$2"\", \"size\": \""$3"\", \"used\": \""$4"\", \"priority\": \""$5"\"}," }' 13 | ) 14 | 15 | echo [ ${result%?} ] 16 | 17 | else 18 | echo [] 19 | fi 20 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/arp_cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | arpCommand=$(command -v arp) 4 | 5 | result=$($arpCommand | awk 'BEGIN {print "["} NR>1 \ 6 | {print "{ \"address\": \"" $1 "\", " \ 7 | "\"hw_type\": \"" $2 "\", " \ 8 | "\"hw_address\": \"" $3 "\", " \ 9 | "\"flags\": \"" $4 "\", " \ 10 | "\"mask\": \"" $5 "\" }, " \ 11 | } \ 12 | END {print "]"}' \ 13 | | /bin/sed 'N;$s/},/}/;P;D') 14 | 15 | if [ -z "$result" ]; then echo {} 16 | else echo $result 17 | fi -------------------------------------------------------------------------------- /main_files/modules/shell_files/cron_history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grepCmd=$(which grep) 4 | cronLog='/var/log/syslog' 5 | numberOfLines='50' 6 | 7 | # Month, Day, Time, Hostname, tag, user, 8 | 9 | result=$($grepCmd -m$numberOfLines CRON $cronLog \ 10 | | awk '{ s = ""; for (i = 6; i <= NF; i++) s = s $i " "; \ 11 | print "{\"time\" : \"" $1" "$2" "$3 "\"," \ 12 | "\"user\" : \"" $6 "\"," \ 13 | "\"message\" : \"" $5" "gensub("\"", "\\\\\"", "g", s) "\"" \ 14 | "}," 15 | }' 16 | ) 17 | 18 | echo [${result%?}] 19 | -------------------------------------------------------------------------------- /main_files/app/line-chart-plugin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
{{ metric.name }}{{ metric.data }}
17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /main_files/app/key-value-list-plugin.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
{{ name }}{{ value }}
18 | 19 |
20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/user_accounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/usr/bin/awk -F: '{ \ 3 | if ($3<=499){userType="system";} \ 4 | else {userType="user";} \ 5 | print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }' < /etc/passwd 6 | ) 7 | 8 | length=$(echo ${#result}) 9 | 10 | if [ $length -eq 0 ]; then 11 | result=$(getent passwd | /usr/bin/awk -F: '{ if ($3<=499){userType="system";} else {userType="user";} print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }') 12 | fi 13 | 14 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /main_files/modules/shell_files/current_ram.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | awkCmd=`which awk` 4 | catCmd=`which cat` 5 | grepCmd=`which grep` 6 | memInfoFile="/proc/meminfo" 7 | 8 | # References: 9 | # Calculations: http://zcentric.com/2012/05/29/mapping-procmeminfo-to-output-of-free-command/ 10 | # Fields: https://www.kernel.org/doc/Documentation/filesystems/proc.txt 11 | 12 | memInfo=`$catCmd $memInfoFile | $grepCmd 'MemTotal\|MemFree\|Buffers\|Cached'` 13 | 14 | echo $memInfo | $awkCmd '{print "{ \"total\": " ($2/1024) ", \"used\": " ( ($2-($5+$8+$11))/1024 ) ", \"free\": " (($5+$8+$11)/1024) " }" }' 15 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ########### Enter Your Redis Password HERE ######### 4 | redisPassword='' 5 | ########### Enter Your Redis Password HERE ######### 6 | 7 | redisCommand=$(which redis-cli); 8 | 9 | if [ -n "$redisPassword" ]; then 10 | redisCommand="$redisCommand -a $redisPassword" 11 | fi 12 | 13 | result=$($redisCommand INFO \ 14 | | grep 'redis_version\|connected_clients\|connected_slaves\|used_memory_human\|total_connections_received\|total_commands_processed' \ 15 | | awk -F: '{print "\"" $1 "\":" "\"" $2 }' \ 16 | | tr '\r' '"' | tr '\n' ',' 17 | ) 18 | echo { ${result%?} } -------------------------------------------------------------------------------- /main_files/modules/shell_files/ip_addresses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | awkCmd=`which awk` 3 | grepCmd=`which grep` 4 | sedCmd=`which sed` 5 | ifconfigCmd=`which ifconfig` 6 | trCmd=`which tr` 7 | digCmd=`which dig` 8 | 9 | externalIp=`$digCmd +short myip.opendns.com @resolver1.opendns.com` 10 | 11 | echo -n "[" 12 | 13 | for item in $($ifconfigCmd | $grepCmd -oP "^[a-zA-Z0-9:]*(?=:)") 14 | do 15 | echo -n "{\"interface\" : \""$item"\", \"ip\" : \"$( $ifconfigCmd $item | $grepCmd "inet" | $awkCmd '{match($0,"inet (addr:)?([0-9.]*)",a)}END{ if (NR != 0){print a[2]; exit}{print "none"}}')\"}, " 16 | done 17 | 18 | echo "{ \"interface\": \"external\", \"ip\": \"$externalIp\" } ]" 19 | -------------------------------------------------------------------------------- /main_files/app/multi-line-chart-plugin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 |
12 |
15 |
16 |
{{ metric.name }}{{ metric.data }}
22 | 23 | 24 |
25 | 26 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/general_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function displaytime { 4 | local T=$1 5 | local D=$((T/60/60/24)) 6 | local H=$((T/60/60%24)) 7 | local M=$((T/60%60)) 8 | local S=$((T%60)) 9 | [[ $D > 0 ]] && printf '%d days ' $D 10 | [[ $H > 0 ]] && printf '%d hours ' $H 11 | [[ $M > 0 ]] && printf '%d minutes ' $M 12 | [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' 13 | printf '%d seconds\n' $S 14 | } 15 | 16 | lsbRelease=$(/usr/bin/lsb_release -ds | sed -e 's/^"//' -e 's/"$//') 17 | uname=$(/bin/uname -r | sed -e 's/^"//' -e 's/"$//') 18 | os=`echo $lsbRelease $uname` 19 | hostname=$(/bin/hostname) 20 | uptime_seconds=$(/bin/cat /proc/uptime | awk '{print $1}') 21 | server_time=$(date) 22 | 23 | echo { \ 24 | \"OS\": \"$os\", \ 25 | \"Hostname\": \"$hostname\", \ 26 | \"Uptime\": \" $(displaytime ${uptime_seconds%.*}) \", \ 27 | \"Server Time\": \"$server_time\" \ 28 | } 29 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/ping.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # http://askubuntu.com/questions/413367/ping-multiple-ips-using-bash 3 | 4 | # get absolute path to config file 5 | SCRIPTPATH=`dirname $(readlink -f $0)` 6 | CONFIG_PATH=$SCRIPTPATH"/../config/ping_hosts" 7 | 8 | catCmd=`which cat` 9 | pingCmd=`which ping` 10 | awkCmd=`which awk` 11 | sedCmd=`which sed` 12 | numOfLinesInConfig=`$sedCmd -n '$=' $CONFIG_PATH` 13 | result='[' 14 | 15 | $catCmd $CONFIG_PATH \ 16 | | while read output 17 | do 18 | singlePing=$($pingCmd -qc 2 $output \ 19 | | $awkCmd -F/ 'BEGIN { endLine="}," } /^rtt/ { if ('$numOfLinesInConfig'==1){endLine="}"} print "{" "\"host\": \"'$output'\", \"ping\": " $5 " " endLine }' \ 20 | ) 21 | numOfLinesInConfig=$(($numOfLinesInConfig-1)) 22 | result=$result$singlePing 23 | if [ $numOfLinesInConfig -eq 0 ] 24 | then 25 | echo $result"]" 26 | fi 27 | done \ 28 | | $sedCmd 's/\},]/}]/g' 29 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/pm2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #get data 4 | command="pm2 list" 5 | data="$($command)" 6 | 7 | #only process data if variable has a length 8 | #this should handle cases where pm2 is not installed 9 | if [ -n "$data" ]; then 10 | 11 | #start processing data on line 4 12 | #don't process last 2 lines 13 | json=$( echo "$data" | tail -n +4 | head -n +2 \ 14 | | awk '{print "{"}\ 15 | {print "\"appName\":\"" $2 "\","} \ 16 | {print "\"id\":\"" $4 "\","} \ 17 | {print "\"mode\":\"" $6 "\","} \ 18 | {print "\"pid\":\"" $8 "\","}\ 19 | {print "\"status\":\"" $10 "\","}\ 20 | {print "\"restart\":\"" $12 "\","}\ 21 | {print "\"uptime\":\"" $14 "\","}\ 22 | {print "\"memory\":\"" $16 $17 "\","}\ 23 | {print "\"watching\":\"" $19 "\""}\ 24 | {print "},"}') 25 | #make sure to remove last comma and print in array 26 | echo "[" ${json%?} "]" 27 | else 28 | #no data found 29 | echo "{}" 30 | fi 31 | 32 | 33 | -------------------------------------------------------------------------------- /main_files/modules/disk-space.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 34 | 35 | 36 | 37 |
NameStatsUsedMount Path
{{partition['file_system']}} 22 | 26 | 27 | 29 | {{ partition['used'] }} / {{ partition['size'] }} 30 | 32 | {{ partition['used%'] }} 33 | {{ partition['mounted'] }}
38 | 39 |
40 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/cpu_utilization.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # by Paul Colby (http://colby.id.au), no rights reserved ;) 3 | 4 | PREV_TOTAL=0 5 | PREV_IDLE=0 6 | iteration=0 7 | 8 | while [[ iteration -lt 2 ]]; do 9 | # Get the total CPU statistics, discarding the 'cpu ' prefix. 10 | CPU=(`sed -n 's/^cpu\s//p' /proc/stat`) 11 | IDLE=${CPU[3]} # Just the idle CPU time. 12 | 13 | # Calculate the total CPU time. 14 | TOTAL=0 15 | for VALUE in "${CPU[@]}"; do 16 | let "TOTAL=$TOTAL+$VALUE" 17 | done 18 | 19 | # Calculate the CPU usage since we last checked. 20 | let "DIFF_IDLE=$IDLE-$PREV_IDLE" 21 | let "DIFF_TOTAL=$TOTAL-$PREV_TOTAL" 22 | let "DIFF_USAGE=(1000*($DIFF_TOTAL-$DIFF_IDLE)/$DIFF_TOTAL+5)/10" 23 | #echo -en "\rCPU: $DIFF_USAGE% \b\b" 24 | 25 | # Remember the total and idle CPU times for the next check. 26 | PREV_TOTAL="$TOTAL" 27 | PREV_IDLE="$IDLE" 28 | 29 | # Wait before checking again. 30 | sleep 1 31 | iteration="$iteration+1" 32 | done 33 | echo -en "$DIFF_USAGE" 34 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/download_transfer_rate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | files=(/sys/class/net/*) 4 | pos=$(( ${#files[*]} - 1 )) 5 | last=${files[$pos]} 6 | 7 | json_output="{" 8 | 9 | for interface in "${files[@]}" 10 | do 11 | basename=$(basename "$interface") 12 | 13 | # find the number of bytes transfered for this interface 14 | in1=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) 15 | 16 | # wait a second 17 | sleep 1 18 | 19 | # check same interface again 20 | in2=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) 21 | 22 | # get the difference (transfer rate) 23 | in_bytes=$((in2 - in1)) 24 | 25 | # convert transfer rate to KB 26 | in_kbytes=$((in_bytes / 1024)) 27 | 28 | # convert transfer rate to KB 29 | json_output="$json_output \"$basename\": $in_kbytes" 30 | 31 | # if it is not the last line 32 | if [[ ! $interface == $last ]] 33 | then 34 | # add a comma to the line (JSON formatting) 35 | json_output="$json_output," 36 | fi 37 | done 38 | 39 | # close the JSON object & print to screen 40 | echo "$json_output}" 41 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/upload_transfer_rate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | files=(/sys/class/net/*) 4 | pos=$(( ${#files[*]} - 1 )) 5 | last=${files[$pos]} 6 | 7 | json_output="{" 8 | 9 | for interface in "${files[@]}" 10 | do 11 | basename=$(basename "$interface") 12 | 13 | # find the number of bytes transfered for this interface 14 | out1=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) 15 | 16 | # wait a second 17 | sleep 1 18 | 19 | # check same interface again 20 | out2=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) 21 | 22 | # get the difference (transfer rate) 23 | out_bytes=$((out2 - out1)) 24 | 25 | # convert transfer rate to KB 26 | out_kbytes=$((out_bytes / 1024)) 27 | 28 | # convert transfer rate to KB 29 | json_output="$json_output \"$basename\": $out_kbytes" 30 | 31 | # if it is not the last line 32 | if [[ ! $interface == $last ]] 33 | then 34 | # add a comma to the line (JSON formatting) 35 | json_output="$json_output," 36 | fi 37 | done 38 | 39 | # close the JSON object & print to screen 40 | echo "$json_output}" 41 | -------------------------------------------------------------------------------- /main_files/app/table-data-plugin.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 |
15 | 16 |
20 | {{ header }} 21 | 22 | {{ (header === sortByColumn && !sortReverse) ? '▲': ''; }} 23 | {{ (header === sortByColumn && sortReverse) ? '▼': ''; }} 24 | 25 |
31 | {{ row[header] }} 32 |
36 | 37 |
38 | 39 | 40 |
41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | plexDash 4 |

5 | 6 |

7 | A system performance dashboard for plex-servers -- linux only! 8 |

9 | 10 |
11 |

12 | Linux Dash screenshot 13 |

14 | 15 |
16 | 17 | ## Features 18 | * A beautiful & simple web-based dashboard for monitoring a plex server 19 | * Passwort protectet! ⇒ **Default password: plexDash** 20 | * Live graphs, refresh-able widgets, and a big range of modules 21 | * Drop-in installation for Linux Servers! 22 | 23 | ## Example Installation for Ubuntu 24 | 25 | 1. Install apache2 and php5: sudo apt-get install apache2 php5 libapache2-mod-php5 php5-mcrypt 26 | 2. Go to the html location: cd /var/www/html 27 | 3. Download the git repo: git clone https://github.com/deepwather/plexDash.git 28 | 4. Set the rigth permissions: chmod -R 775 /var/www/html/plexDash 29 | 5. And as well the right owner: chown -R www-data:www-data /var/www/html/plexDash 30 | 6. Enjoy! Login over browser, enter default password: plexDash (To change the default pw, edit config/login.php) 31 | 32 | ## Support 33 | 34 | The following distributions are supported: 35 | * Arch 36 | * Debian 6,7 37 | * Ubuntu 11.04+ 38 | * Linux Mint 16+ 39 | * CentOS 5, 6 40 | * openSUSE 41 | -------------------------------------------------------------------------------- /css/login.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | body { 7 | background: #2d2d2d url('../img/splash.png') no-repeat center center fixed; 8 | -webkit-background-size: cover; 9 | -moz-background-size: cover; 10 | -o-background-size: cover; 11 | background-size: cover; 12 | } 13 | body, a { 14 | color: #fff; 15 | font-family: 'Open Sans', sans-serif; 16 | font-size: 13px; 17 | } 18 | form { 19 | background: #141414; 20 | opacity:0.5; 21 | position: absolute; 22 | overflow: hidden; 23 | line-height: 2em; 24 | padding: 5px; 25 | width: 251px; 26 | bottom: 0px; 27 | right: 0px; 28 | } 29 | 30 | .menu { 31 | border:1px outset gray; margin:.5em; padding:.0em; background: #2d2d2d; 32 | } 33 | 34 | .logout { 35 | border:1px outset gray; 36 | margin:0.5em; 37 | padding:0em; 38 | background-color:#2d2d2d; 39 | width: 40px; 40 | position: absolute; 41 | left: 205px; 42 | top: 150px; 43 | } 44 | label { 45 | clear: both; 46 | float: right; 47 | color: black; 48 | } 49 | span { 50 | float: left; 51 | padding-right: 5px; 52 | text-align: right; 53 | width: 8em; 54 | color: white; 55 | } 56 | input { 57 | float: left; 58 | margin-top: 3px; 59 | } 60 | input.text { 61 | border: 1px solid #111; 62 | padding: 2px; 63 | width: 10em; 64 | } 65 | input.submit { 66 | clear: both; 67 | float: right; 68 | } 69 | h2 { 70 | font-size: 13px; 71 | font-weight: normal; 72 | margin: 0px; 73 | text-align: right; 74 | } 75 | p { 76 | display: none; 77 | } 78 | 79 | textarea, input[type="password"] { 80 | background-color: white; 81 | } 82 | -------------------------------------------------------------------------------- /css/css.css: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Open Sans'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 8 | } 9 | /* cyrillic */ 10 | @font-face { 11 | font-family: 'Open Sans'; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 16 | } 17 | /* greek-ext */ 18 | @font-face { 19 | font-family: 'Open Sans'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 23 | unicode-range: U+1F00-1FFF; 24 | } 25 | /* greek */ 26 | @font-face { 27 | font-family: 'Open Sans'; 28 | font-style: normal; 29 | font-weight: 400; 30 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 31 | unicode-range: U+0370-03FF; 32 | } 33 | /* vietnamese */ 34 | @font-face { 35 | font-family: 'Open Sans'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 39 | unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; 40 | } 41 | /* latin-ext */ 42 | @font-face { 43 | font-family: 'Open Sans'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); 47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 48 | } 49 | /* latin */ 50 | @font-face { 51 | font-family: 'Open Sans'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2) format('woff2'); 55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 56 | } 57 | -------------------------------------------------------------------------------- /main_files/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = require('express')(); 3 | var server = require('http').Server(app); 4 | var path = require('path'); 5 | var spawn = require('child_process').spawn; 6 | var fs = require('fs'); 7 | var ws = require('websocket').server; 8 | 9 | server.listen(80); 10 | console.log('Linux Dash Server Started!'); 11 | 12 | app.use(express.static(path.resolve(__dirname + '/../'))); 13 | 14 | app.get('/', function (req, res) { 15 | res.sendFile(path.resolve(__dirname + '/../index.html')); 16 | }); 17 | 18 | app.get('/websocket', function (req, res) { 19 | 20 | res.send({ 21 | websocket_support: true, 22 | }); 23 | 24 | }); 25 | 26 | wsServer = new ws({ 27 | httpServer: server 28 | }); 29 | 30 | function getShellFilePath(moduleName) { 31 | return __dirname + '/modules/shell_files/' + moduleName + '.sh'; 32 | } 33 | 34 | function shellPathAndModuleNameAreValid(shellFilePath, moduleName) { 35 | 36 | var moduleInvalidName = moduleName.indexOf('.') > -1; 37 | var moduleNameEmpty = !moduleName; 38 | var moduleNotFound = !fs.existsSync(shellFilePath); 39 | var isValid = true; 40 | 41 | if (moduleInvalidName || moduleNameEmpty || moduleNotFound) { 42 | isValid = false; 43 | } 44 | 45 | return isValid; 46 | } 47 | 48 | wsServer.on('request', function(request) { 49 | 50 | var wsClient = request.accept('plex-dash', request.origin); 51 | 52 | wsClient.on('message', function(wsReq) { 53 | 54 | var moduleName = wsReq.utf8Data; 55 | var shellFile = getShellFilePath(moduleName); 56 | 57 | if (!shellPathAndModuleNameAreValid(shellFile, moduleName)) { 58 | return; 59 | } 60 | 61 | var command = spawn(shellFile, [ wsReq.color || '' ]); 62 | var output = []; 63 | 64 | command.stdout.on('data', function(chunk) { 65 | output.push(chunk); 66 | }); 67 | 68 | command.on('close', function(code) { 69 | 70 | if (code === 0) { 71 | 72 | var wsResponse = { 73 | moduleName: moduleName, 74 | output: output.toString(), 75 | }; 76 | 77 | wsClient.sendUTF(JSON.stringify(wsResponse)); 78 | } 79 | 80 | }); 81 | 82 | }); 83 | 84 | }); 85 | 86 | app.get('/main_files/', function (req, res) { 87 | 88 | var shellFile = getShellFilePath(req.query.module); 89 | 90 | if (!shellPathAndModuleNameAreValid(shellFile, req.query.module)) { 91 | res.sendStatus(406); 92 | return; 93 | } 94 | 95 | var command = spawn(shellFile, [ req.query.color || '' ]); 96 | var output = []; 97 | 98 | command.stdout.on('data', function(chunk) { 99 | output.push(chunk); 100 | }); 101 | 102 | command.on('close', function(code) { 103 | if (code === 0) res.send(output.toString()); 104 | else res.sendStatus(500); 105 | }); 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /main_files/modules/shell_files/scheduled_crons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###### 3 | # Credit: http://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users#answer-137173 4 | ###### 5 | 6 | catCmd=`which cat` 7 | awkCmd=`which awk` 8 | sedCmd=`which sed` 9 | egrepCmd=`which egrep` 10 | echoCmd=`which echo` 11 | crontabCmd=`which crontab` 12 | trCmd=`which tr` 13 | 14 | # System-wide crontab file and cron job directory. Change these for your system. 15 | CRONTAB='/etc/crontab' 16 | CRONDIR='/etc/cron.d' 17 | 18 | # Single tab character. Annoyingly necessary. 19 | tab=$(echo -en "\t") 20 | 21 | # Given a stream of crontab lines, exclude non-cron job lines, replace 22 | # whitespace characters with a single space, and remove any spaces from the 23 | # beginning of each line. 24 | function clean_cron_lines() { 25 | while read line ; do 26 | $echoCmd "${line}" | 27 | $egrepCmd --invert-match '^($|\s*#|\s*[[:alnum:]_]+=)' | 28 | $sedCmd --regexp-extended "s/\s+/ /g" | 29 | $sedCmd --regexp-extended "s/^ //" 30 | done; 31 | } 32 | 33 | # Given a stream of cleaned crontab lines, $echoCmd any that don't include the 34 | # run-parts command, and for those that do, show each job file in the run-parts 35 | # directory as if it were scheduled explicitly. 36 | function lookup_run_parts() { 37 | while read line ; do 38 | match=$($echoCmd "${line}" | $egrepCmd -o 'run-parts (-{1,2}\S+ )*\S+') 39 | 40 | if [[ -z "${match}" ]] ; then 41 | $echoCmd "${line}" 42 | else 43 | cron_fields=$($echoCmd "${line}" | cut -f1-6 -d' ') 44 | cron_job_dir=$($echoCmd "${match}" | awk '{print $NF}') 45 | 46 | if [[ -d "${cron_job_dir}" ]] ; then 47 | for cron_job_file in "${cron_job_dir}"/* ; do # */ 48 | [[ -f "${cron_job_file}" ]] && $echoCmd "${cron_fields} ${cron_job_file}" 49 | done 50 | fi 51 | fi 52 | done; 53 | } 54 | 55 | # Temporary file for crontab lines. 56 | temp=$(mktemp) || exit 1 57 | 58 | # Add all of the jobs from the system-wide crontab file. 59 | $catCmd "${CRONTAB}" | clean_cron_lines | lookup_run_parts >"${temp}" 60 | 61 | # Add all of the jobs from the system-wide cron directory. 62 | $catCmd "${CRONDIR}"/* | clean_cron_lines >>"${temp}" # */ 63 | 64 | # Add each user's crontab (if it exists). Insert the user's name between the 65 | # five time fields and the command. 66 | while read user ; do 67 | $crontabCmd -l -u "${user}" 2>/dev/null | 68 | clean_cron_lines | 69 | $sedCmd --regexp-extended "s/^((\S+ +){5})(.+)$/\1${user} \3/" >>"${temp}" 70 | done < <(cut --fields=1 --delimiter=: /etc/passwd) 71 | 72 | # Output the collected crontab lines. 73 | 74 | ## Changes: Parses output into JSON 75 | 76 | $catCmd "${temp}" \ 77 | | awk 'BEGIN {print "["} \ 78 | {print "{ \"min(s)\": \"" $1 \ 79 | "\", \"hours(s)\": \"" $2 "\", " \ 80 | " \"day(s)\": \"" $3 "\", " \ 81 | " \"month\": \"" $4 "\", " \ 82 | " \"weekday\": \"" $5 "\", " \ 83 | " \"user\": \"" $6 "\", " \ 84 | " \"command\": \""} \ 85 | {for(i=7;i<=NF;++i) printf("%s ", gensub("\"", "\\\\\"", "g", $i) ) } \ 86 | {print "\" " \ 87 | "}," } \ 88 | END {print "]"}' \ 89 | | $sedCmd 'N;$s/,\n//;P;D' | $trCmd -s '\n' ' ' 90 | 91 | rm --force "${temp}" 92 | -------------------------------------------------------------------------------- /css/plexDash.css: -------------------------------------------------------------------------------- 1 | html body { 2 | margin: 0; 3 | } 4 | 5 | html body .hero { 6 | background: #00BA8B; 7 | color: #ffffff; 8 | padding: 0; 9 | } 10 | html body .hero h4 { 11 | color: #ffffff; 12 | display: inline-block; 13 | font-size: 20px; 14 | font-weight: 600; 15 | height: 40px; 16 | line-height: 35px; 17 | margin: 0; 18 | vertical-align: middle; 19 | } 20 | html body .hero small { 21 | letter-spacing: 0.1rem; 22 | line-height: 40px; 23 | margin-left: 20px; 24 | opacity: 0.9; 25 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 26 | } 27 | 28 | html body #plugins { 29 | display: flex; 30 | flex-flow: row wrap; 31 | justify-content: center; 32 | } 33 | html body .plugin { 34 | border: 1px solid #3d3d3d; 35 | border-radius: 0 0 5px 5px; 36 | box-shadow: none; 37 | margin: 10px; 38 | background-color: #2F2F2F; 39 | } 40 | html body .plugin .top-bar { 41 | background: #212121 0px 0px; 42 | border-bottom: 1px solid #3d3d3d; 43 | color: #999; 44 | font-size: 14px; 45 | font-weight: bold; 46 | height: 40px; 47 | line-height: 40px; 48 | padding: 0 0 0 15px; 49 | position: relative; 50 | text-align: left; 51 | text-transform: none; 52 | } 53 | html body .plugin .top-bar last-update { 54 | float: right; 55 | margin: 0 10px; 56 | opacity: 0.8; 57 | } 58 | html body .plugin .top-bar refresh-btn { 59 | float: right; 60 | } 61 | html body .plugin .top-bar refresh-btn button { 62 | background: #282828; 63 | border: 1px solid rgba(61, 61, 61, 61); 64 | border-radius: 4px; 65 | box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1) inset, 0px 1px 2px rgba(0, 0, 0, 0.1); 66 | color: #999999; 67 | cursor: pointer; 68 | box-shadow: none; 69 | display: inline-block; 70 | float: none; 71 | font-size: 14px; 72 | height: 39px; 73 | margin: 0 -5px 0 10px; 74 | padding: 0 4px; 75 | width: auto; 76 | } 77 | html body .plugin .top-bar refresh-btn button:hover { 78 | background: #333; 79 | } 80 | html body .plugin .plugin-body { 81 | border: none; 82 | height: auto; 83 | line-height: normal; 84 | max-height: 304px; 85 | padding: 0; 86 | width: 100%; 87 | } 88 | html body .plugin .plugin-body table { 89 | border-collapse: separate; 90 | border-spacing: 0; 91 | font-size: 12px; 92 | min-width: 300px; 93 | } 94 | html body .plugin .plugin-body table tr:not(:first-child) th { 95 | border-top: 1px solid #3d3d3d; 96 | } 97 | html body .plugin .plugin-body table td, 98 | html body .plugin .plugin-body table th { 99 | border-bottom: none; 100 | padding: 4px 5px; 101 | } 102 | html body .plugin .plugin-body table td:not(:first-child), 103 | html body .plugin .plugin-body table th:not(:first-child) { 104 | border-left: 1px solid #564F4F; 105 | } 106 | html body .plugin .plugin-body table th:not(.filter-container) { 107 | background: transparent -moz-linear-gradient(center top, #fafafa 0%, #e9e9e9 100%) repeat scroll 0% 0%; 108 | text-transform: uppercase; 109 | color: #fff; 110 | } 111 | html body .plugin .plugin-body table th.filter-container .filter { 112 | border-bottom: none; 113 | font-size: 12px; 114 | height: auto; 115 | padding: 2px; 116 | color: #fff; 117 | } 118 | html body .plugin .plugin-body table th.filter-container .filter::before { 119 | content: 'Search >'; 120 | opacity: 0.5; 121 | position: absolute; 122 | left: 0; 123 | } 124 | html body .plugin .plugin-body table td { 125 | border-top: 1px solid #3d3d3d; 126 | } 127 | html body .plugin canvas { 128 | height: 200px; 129 | /*width: 400px;*/ 130 | } 131 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | plexDash | stable v1.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 |
44 | 67 |
68 |
69 |
70 |
71 |
72 | System Information 73 |
74 | 75 |
76 |
77 |
78 | 87 |
88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /config/login.php: -------------------------------------------------------------------------------- 1 | 'ro5otj57', 20 | 'test' => 'samplepass', 21 | 'admin' => 'passwd' 22 | ); 23 | 24 | -------------------------------------------------------------------- 25 | SAMPLE if you only want to request only password on login form. 26 | Note: only passwords are listed 27 | 28 | $LOGIN_INFORMATION = array( 29 | 'ro5otj57', 30 | 'samplepass', 31 | 'passwd' 32 | ); 33 | 34 | -------------------------------------------------------------------- 35 | */ 36 | 37 | ################################################################## 38 | # SETTINGS START 39 | ################################################################## 40 | 41 | // Add login/password pairs below, like described above 42 | // NOTE: all rows except last must have comma "," at the end of line 43 | $LOGIN_INFORMATION = array( 44 | 'plexDash' 45 | ); 46 | 47 | // request login? true - show login and password boxes, false - password box only 48 | define('USE_USERNAME', false); 49 | 50 | // User will be redirected to this page after logout 51 | define('LOGOUT_URL', 'index.php'); 52 | 53 | // time out after NN minutes of inactivity. Set to 0 to not timeout 54 | define('TIMEOUT_MINUTES', 0); 55 | 56 | // This parameter is only useful when TIMEOUT_MINUTES is not zero 57 | // true - timeout time from last activity, false - timeout time from login 58 | define('TIMEOUT_CHECK_ACTIVITY', true); 59 | 60 | ################################################################## 61 | # SETTINGS END 62 | ################################################################## 63 | 64 | 65 | /////////////////////////////////////////////////////// 66 | // do not change code below 67 | /////////////////////////////////////////////////////// 68 | 69 | // show usage example 70 | if(isset($_GET['help'])) { 71 | die('Include following code into every page you would like to protect, at the very beginning (first line):
<?php include("' . str_replace('\\','\\\\',__FILE__) . '"); ?>'); 72 | } 73 | 74 | // timeout in seconds 75 | $timeout = (TIMEOUT_MINUTES == 0 ? 0 : time() + TIMEOUT_MINUTES * 60); 76 | 77 | // logout? 78 | if(isset($_GET['logout'])) { 79 | setcookie("verify", '', $timeout, '/'); // clear password; 80 | header('Location: ' . LOGOUT_URL); 81 | exit(); 82 | } 83 | 84 | if(!function_exists('showLoginPasswordProtect')) { 85 | 86 | // show login form 87 | function showLoginPasswordProtect($error_msg) { 88 | ?> 89 | 90 | 91 | Welcome to plexDash! 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 |
password:
'; ?> 105 |
108 |
109 |
110 | 111 | 112 | 113 | 114 | $val) { 151 | $lp = (USE_USERNAME ? $key : '') .'%'.$val; 152 | if ($_COOKIE['verify'] == md5($lp)) { 153 | $found = true; 154 | // prolong timeout 155 | if (TIMEOUT_CHECK_ACTIVITY) { 156 | setcookie("verify", md5($lp), $timeout, '/'); 157 | } 158 | break; 159 | } 160 | } 161 | if (!$found) { 162 | showLoginPasswordProtect(""); 163 | } 164 | } 165 | 166 | ?> -------------------------------------------------------------------------------- /js/smoothie.min.js: -------------------------------------------------------------------------------- 1 | (function(e){function n(e){this.options=t.extend({},n.defaultOptions,e);this.clear()}function r(e){this.options=t.extend({},r.defaultChartOptions,e);this.seriesSet=[];this.currentValueRange=1;this.currentVisMinValue=0;this.lastRenderTimeMillis=0}var t={extend:function(){arguments[0]=arguments[0]||{};for(var e=1;ethis.maxValue){this.maxValue=t}if(t=0&&this.data[r][0]>e){r--}if(r===-1){this.data.splice(0,0,[e,t])}else if(this.data.length>0&&this.data[r][0]===e){if(n){this.data[r][1]+=t;t=this.data[r][1]}else{this.data[r][1]=t}}else if(r=t&&this.data[n+1][0]0){e.resetBoundsTimerId=setInterval(function(){e.resetBounds()},e.options.resetBoundsInterval)}};r.prototype.removeTimeSeries=function(e){var t=this.seriesSet.length;for(var n=0;n.1||Math.abs(a)>.1;this.currentValueRange+=e.scaleSmoothing*u;this.currentVisMinValue+=e.scaleSmoothing*a}this.valueRange={min:n,max:t}};r.prototype.render=function(e,t){var n=(new Date).getTime();if(!this.isAnimatingScale){var r=Math.min(1e3/6,this.options.millisPerPixel);if(n-this.lastRenderTimeMillis0){i.beginPath();for(var l=t-t%s.grid.millisPerLine;l>=u;l-=s.grid.millisPerLine){var c=f(l);if(s.grid.sharpLines){c-=.5}i.moveTo(c,0);i.lineTo(c,o.height)}i.stroke();i.closePath()}for(var h=1;h1){if(w.fillStyle){i.lineTo(o.width+w.lineWidth+1,x);i.lineTo(o.width+w.lineWidth+1,o.height+w.lineWidth+1);i.lineTo(E,o.height+w.lineWidth);i.fillStyle=w.fillStyle;i.fill()}if(w.strokeStyle&&w.strokeStyle!=="none"){i.stroke()}i.closePath()}i.restore()}if(!s.labels.disabled&&!isNaN(this.valueRange.min)&&!isNaN(this.valueRange.max)){var k=s.yMaxFormatter(this.valueRange.max,s.labels.precision),L=s.yMinFormatter(this.valueRange.min,s.labels.precision);i.fillStyle=s.labels.fillStyle;i.fillText(k,o.width-i.measureText(k).width-2,s.labels.fontSize);i.fillText(L,o.width-i.measureText(L).width-2,o.height-2)}if(s.timestampFormatter&&s.grid.millisPerLine>0){var A=o.width-i.measureText(L).width+4;for(var l=t-t%s.grid.millisPerLine;l>=u;l-=s.grid.millisPerLine){var c=f(l);if(c div { 197 | background-color: #af6c17; 198 | height: 100%; 199 | width: 6px; 200 | display: inline-block; 201 | 202 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 203 | animation: stretchdelay 1.2s infinite ease-in-out; 204 | } 205 | 206 | .spinner .rect2 { 207 | -webkit-animation-delay: -1.1s; 208 | animation-delay: -1.1s; 209 | } 210 | 211 | .spinner .rect3 { 212 | -webkit-animation-delay: -1.0s; 213 | animation-delay: -1.0s; 214 | } 215 | 216 | .spinner .rect4 { 217 | -webkit-animation-delay: -0.9s; 218 | animation-delay: -0.9s; 219 | } 220 | 221 | .spinner .rect5 { 222 | -webkit-animation-delay: -0.8s; 223 | animation-delay: -0.8s; 224 | } 225 | 226 | @-webkit-keyframes stretchdelay { 227 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 228 | 20% { -webkit-transform: scaleY(1.0) } 229 | } 230 | 231 | @keyframes stretchdelay { 232 | 0%, 40%, 100% { 233 | transform: scaleY(0.4); 234 | -webkit-transform: scaleY(0.4); 235 | } 20% { 236 | transform: scaleY(1.0); 237 | -webkit-transform: scaleY(1.0); 238 | } 239 | } 240 | 241 | /**** General Elements ****/ 242 | table 243 | { 244 | width: 100%; 245 | font-size: 10px; 246 | margin: 0; 247 | border-collapse: collapse; 248 | text-align: left; 249 | table-layout:fixed; 250 | } 251 | table th, 252 | table td { 253 | padding: 5px; 254 | max-width: 250px; 255 | word-wrap: break-word; 256 | } 257 | table th 258 | { 259 | font-weight: 600; 260 | text-transform: uppercase; 261 | border-bottom: 1px solid #f1f1f1; 262 | } 263 | table td { 264 | border-bottom: 1px solid #f1f1f1; 265 | padding: 9px 8px; 266 | font-family: Arial, sans-serif; 267 | font-size: 11px; 268 | /*letter-spacing: .1em;*/ 269 | color: #fff; 270 | } 271 | table tbody tr:hover td 272 | { 273 | background-color: #404040; 274 | } 275 | table.metrics-table { 276 | text-align: center; 277 | } 278 | canvas { 279 | float: none; 280 | margin: 0 auto; 281 | width: 100%; 282 | max-width: 100%; 283 | } 284 | /********************************************* 285 | Widget Elements 286 | *********************************************/ 287 | .progress-bar { 288 | background-color: #eec; 289 | border-radius: 10px; /* (height of inner div) / 2 + padding */ 290 | padding: 0px; 291 | clear: both; 292 | display: inline-block; 293 | overflow: hidden; 294 | white-space: nowrap; 295 | } 296 | .progress-bar > div { 297 | background-color: #1EAEDB; 298 | width: 0%; 299 | height: 5px; 300 | border-radius: 5px; 301 | } 302 | .table-data-plugin .filter-container { 303 | padding-bottom: 0; 304 | margin: 0; 305 | } 306 | .table-data-plugin .filter, 307 | .table-data-plugin .filter:focus, 308 | .table-data-plugin .filter:active { 309 | height: 20px; 310 | padding: 5px; 311 | margin: 5px; 312 | border: none; 313 | outline-color: transparent; 314 | background: transparent; 315 | width: 100%; 316 | margin: 0; 317 | text-align: center; 318 | font-size: 15px; 319 | } 320 | .table-data-plugin .filter:focus { 321 | border-bottom: 1px solid #ff5722; 322 | } 323 | .table-data-plugin thead tr th a, 324 | .table-data-plugin thead tr th a:visited { 325 | text-decoration: none; 326 | } 327 | .table-data-plugin .column-sort-caret { 328 | font-size: 10px; 329 | color: #1EAEDB; 330 | } 331 | /* 332 | * Popover 333 | * http://codepen.io/derekpcollins/pen/JCLhG/ 334 | */ 335 | 336 | /* The element to hover over */ 337 | .qs { 338 | cursor: default; 339 | display: inline-block; 340 | position: relative; 341 | } 342 | .qs .popover { 343 | text-transform: none; 344 | background-color: rgba(0, 0, 0, 0.85); 345 | border-radius: 5px; 346 | bottom: 42px; 347 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); 348 | color: #fff; 349 | display: none; 350 | font-size: 12px; 351 | font-family: 'Helvetica',sans-serif; 352 | padding: 7px 10px; 353 | position: absolute; 354 | width: 200px; 355 | z-index: 4; 356 | } 357 | .qs .popover:before { 358 | border-top: 7px solid rgba(0, 0, 0, 0.85); 359 | border-right: 7px solid transparent; 360 | border-left: 7px solid transparent; 361 | bottom: -7px; 362 | content: ''; 363 | display: block; 364 | left: 50%; 365 | margin-left: -7px; 366 | position: absolute; 367 | } 368 | .qs:hover .popover { 369 | display: block; 370 | -webkit-animation: fade-in .3s linear 1, move-up .3s linear 1; 371 | -moz-animation: fade-in .3s linear 1, move-up .3s linear 1; 372 | -ms-animation: fade-in .3s linear 1, move-up .3s linear 1; 373 | } 374 | 375 | @-webkit-keyframes fade-in { 376 | from { 377 | opacity: 0; 378 | } 379 | to { 380 | opacity: 1; 381 | } 382 | } 383 | @-moz-keyframes fade-in { 384 | from { 385 | opacity: 0; 386 | } 387 | to { 388 | opacity: 1; 389 | } 390 | } 391 | @-ms-keyframes fade-in { 392 | from { 393 | opacity: 0; 394 | } 395 | to { 396 | opacity: 1; 397 | } 398 | } 399 | @-webkit-keyframes move-up { 400 | from { 401 | bottom: 30px; 402 | } 403 | to { 404 | bottom: 42px; 405 | } 406 | } 407 | @-moz-keyframes move-up { 408 | from { 409 | bottom: 30px; 410 | } 411 | to { 412 | bottom: 42px; 413 | } 414 | } 415 | @-ms-keyframes move-up { 416 | from { 417 | bottom: 30px; 418 | } 419 | to { 420 | bottom: 42px; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /js/modules.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | "use strict"; 4 | 5 | /////////////////////////////////////////////////////////// 6 | ////////////////// Module Directives /////////////////// // 7 | /////////////////////////////////////////////////////////// 8 | 9 | angular.module('plexDash').directive('diskSpace', ['server', function(server) { 10 | return { 11 | restrict: 'E', 12 | scope: {}, 13 | templateUrl: 'main_files/modules/disk-space.html', 14 | link: function(scope) { 15 | 16 | scope.heading = "Disk Partitions"; 17 | 18 | scope.getData = function() { 19 | server.get('disk_partitions', function(serverResponseData) { 20 | scope.diskSpaceData = serverResponseData; 21 | }); 22 | 23 | scope.lastGet = new Date().getTime(); 24 | }; 25 | 26 | scope.getData(); 27 | 28 | scope.getKB = function(stringSize) { 29 | var lastChar = stringSize.slice(-1), 30 | size = parseFloat(stringSize.replace(",", ".")); 31 | 32 | switch (lastChar) { 33 | case 'M': 34 | return size * Math.pow(1024, 1); 35 | case 'G': 36 | return size * Math.pow(1024, 2); 37 | case 'T': 38 | return size * Math.pow(1024, 3); 39 | case 'P': 40 | return size * Math.pow(1024, 4); 41 | case 'E': 42 | return size * Math.pow(1024, 5); 43 | case 'Z': 44 | return size * Math.pow(1024, 6); 45 | case 'Y': 46 | return size * Math.pow(1024, 7); 47 | default: 48 | return size; 49 | } 50 | }; 51 | } 52 | }; 53 | }]); 54 | 55 | angular.module('plexDash').directive('ramChart', ['server', function(server) { 56 | return { 57 | restrict: 'E', 58 | scope: {}, 59 | templateUrl: 'main_files/modules/ram-chart.html', 60 | link: function(scope) { 61 | 62 | // get max ram available on machine before we 63 | // can start charting 64 | server.get('current_ram', function(resp) { 65 | scope.maxRam = resp.total; 66 | scope.minRam = 0; 67 | }); 68 | 69 | scope.ramToDisplay = function(serverResponseData) { 70 | return serverResponseData.used; 71 | }; 72 | 73 | var humanizeRam = function (ramInMB) { 74 | var ram = { 75 | value: parseInt(ramInMB, 10), 76 | unit: 'MB', 77 | }; 78 | 79 | // if ram > 1,000 MB, use GB 80 | if (ram.value > 1000) { 81 | ram = { 82 | value: (ramInMB/1024).toFixed(2), 83 | unit: 'GB', 84 | }; 85 | } 86 | 87 | return ram.value + ' ' + ram.unit; 88 | }; 89 | 90 | scope.ramMetrics = [{ 91 | name: 'Used', 92 | generate: function(serverResponseData) { 93 | var ratio = serverResponseData.used / serverResponseData.total; 94 | var percentage = parseInt(ratio * 100); 95 | 96 | var usedRam = humanizeRam(serverResponseData.used); 97 | return usedRam + ' (' + percentage.toString() + '%)'; 98 | } 99 | }, 100 | { 101 | name: 'Free', 102 | generate: function(serverResponseData) { 103 | 104 | var freeRam = humanizeRam(serverResponseData.free); 105 | var totalRam = humanizeRam(serverResponseData.total); 106 | return freeRam + ' of ' + totalRam; 107 | } 108 | }]; 109 | } 110 | }; 111 | }]); 112 | 113 | angular.module('plexDash').directive('cpuAvgLoadChart', ['server', function(server) { 114 | return { 115 | restrict: 'E', 116 | scope: {}, 117 | templateUrl: 'main_files/modules/cpu-load.html', 118 | link: function(scope) { 119 | scope.units = '%'; 120 | } 121 | }; 122 | }]); 123 | 124 | angular.module('plexDash').directive('cpuUtilizationChart', ['server', function(server) { 125 | return { 126 | restrict: 'E', 127 | scope: {}, 128 | templateUrl: 'main_files/modules/cpu-utilization-chart.html', 129 | link: function(scope) { 130 | scope.min = 0; 131 | scope.max = 100; 132 | 133 | scope.displayValue = function(serverResponseData) { 134 | return serverResponseData; 135 | }; 136 | 137 | scope.utilMetrics = [{ 138 | name: 'Usage', 139 | generate: function(serverResponseData) { 140 | return serverResponseData + ' %'; 141 | } 142 | }]; 143 | 144 | } 145 | }; 146 | }]); 147 | 148 | angular.module('plexDash').directive('uploadTransferRateChart', ['server', function(server) { 149 | return { 150 | restrict: 'E', 151 | scope: {}, 152 | templateUrl: 'main_files/modules/upload-transfer-rate.html', 153 | link: function(scope) { 154 | scope.delay = 2000; 155 | scope.units = 'KB/s'; 156 | } 157 | }; 158 | }]); 159 | 160 | angular.module('plexDash').directive('downloadTransferRateChart', ['server', function(server) { 161 | return { 162 | restrict: 'E', 163 | scope: {}, 164 | templateUrl: 'main_files/modules/download-transfer-rate.html', 165 | link: function(scope) { 166 | scope.delay = 2000; 167 | scope.units = 'KB/s'; 168 | } 169 | }; 170 | }]); 171 | 172 | ////////////////////////////////////////////////////////// 173 | /////////////// Table Data Modules //////////////////// // 174 | ////////////////////////////////////////////////////////// 175 | var simpleTableModules = [ 176 | { 177 | name: 'machineInfo', 178 | template: '' 179 | }, 180 | { 181 | name: 'ipAddresses', 182 | template: '' 183 | }, 184 | { 185 | name: 'ramIntensiveProcesses', 186 | template: '' 187 | }, 188 | { 189 | name: 'cpuIntensiveProcesses', 190 | template: '' 191 | }, 192 | { 193 | name: 'networkConnections', 194 | template: '' 195 | }, 196 | { 197 | name: 'serverAccounts', 198 | template: '' 199 | }, 200 | { 201 | name: 'loggedInAccounts', 202 | template: '' 203 | }, 204 | { 205 | name: 'recentLogins', 206 | template: '' 207 | }, 208 | { 209 | name: 'arpCacheTable', 210 | template: '' 211 | }, 212 | { 213 | name: 'commonApplications', 214 | template: '' 215 | }, 216 | { 217 | name: 'pingSpeeds', 218 | template: '' 219 | }, 220 | { 221 | name: 'bandwidth', 222 | template: '' 223 | }, 224 | { 225 | name: 'swapUsage', 226 | template: '' 227 | }, 228 | { 229 | name: 'internetSpeed', 230 | template: '' 231 | }, 232 | { 233 | name: 'memcached', 234 | template: '' 235 | }, 236 | { 237 | name: 'redis', 238 | template: '' 239 | }, 240 | { 241 | name: 'pm2', 242 | template: '' 243 | }, 244 | { 245 | name: 'memoryInfo', 246 | template: '' 247 | }, 248 | { 249 | name: 'cpuInfo', 250 | template: '' 251 | }, 252 | { 253 | name: 'ioStats', 254 | template: '' 255 | }, 256 | { 257 | name: 'scheduledCrons', 258 | template: '' 259 | }, 260 | { 261 | name: 'cronHistory', 262 | template: '' 263 | } 264 | ]; 265 | 266 | simpleTableModules.forEach(function(module, key) { 267 | 268 | angular.module('plexDash').directive(module.name, ['server', function(server) { 269 | 270 | var moduleDirective = { 271 | restrict: 'E', 272 | scope: {} 273 | }; 274 | 275 | if (module.templateUrl) { 276 | moduleDirective['templateUrl'] = 'main_files/modules/' + module.templateUrl 277 | } 278 | 279 | if (module.template) { 280 | moduleDirective['template'] = module.template; 281 | } 282 | 283 | return moduleDirective; 284 | }]); 285 | 286 | }); 287 | 288 | }()); 289 | -------------------------------------------------------------------------------- /js/plexDash.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | angular.module('plexDash', ['ngRoute']); 4 | 5 | /** 6 | * Routes for different tabs on UI 7 | */ 8 | angular.module('plexDash').config(['$routeProvider', 9 | function($routeProvider) { 10 | 11 | $routeProvider. 12 | when('/loading', { 13 | templateUrl: 'main_files/app/loading.html', 14 | controller: function appLoadController ($scope, $location, $rootScope) { 15 | 16 | var loadUrl = localStorage.getItem('currentTab') || 'system-status'; 17 | 18 | var loadplexDash = function () { 19 | $location.path(loadUrl); 20 | }; 21 | 22 | $rootScope.$on('start-plex-dash', loadplexDash); 23 | 24 | }, 25 | }). 26 | when('/system-status', { 27 | templateUrl: 'main_files/sections/system-status.html', 28 | }). 29 | when('/basic-info', { 30 | templateUrl: 'main_files/sections/basic-info.html', 31 | }). 32 | when('/network', { 33 | templateUrl: 'main_files/sections/network.html', 34 | }). 35 | when('/accounts', { 36 | templateUrl: 'main_files/sections/accounts.html', 37 | }). 38 | when('/apps', { 39 | templateUrl: 'main_files/sections/applications.html', 40 | }). 41 | otherwise({ 42 | redirectTo: '/loading' 43 | }); 44 | 45 | } 46 | ]); 47 | 48 | 49 | /** 50 | * Service which gets data from server 51 | * via HTTP or Websocket (if supported) 52 | */ 53 | angular.module('plexDash').service('server', ['$http', '$rootScope', '$location', function($http, $rootScope, $location) { 54 | 55 | var websocket = { 56 | connection: null, 57 | onMessageEventHandlers: {} 58 | }; 59 | 60 | /** 61 | * @description: 62 | * Establish a websocket connection with server 63 | * 64 | * @return Null 65 | */ 66 | var establishWebsocketConnection = function() { 67 | 68 | var websocketUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.hostname + ':' + window.location.port; 69 | 70 | if (websocket.connection === null) { 71 | 72 | websocket.connection = new WebSocket(websocketUrl, 'plex-dash'); 73 | 74 | websocket.connection.onopen = function() { 75 | $rootScope.$broadcast("start-plex-dash", {}); 76 | $rootScope.$apply(); 77 | console.info('Websocket connection is open'); 78 | }; 79 | 80 | websocket.connection.onmessage = function(event) { 81 | 82 | var response = JSON.parse(event.data); 83 | var moduleName = response.moduleName; 84 | var moduleData = JSON.parse(response.output); 85 | 86 | if (!!websocket.onMessageEventHandlers[moduleName]) { 87 | websocket.onMessageEventHandlers[moduleName](moduleData); 88 | } else { 89 | console.info("Websocket could not find module", moduleName, "in:", websocket.onMessageEventHandlers); 90 | } 91 | 92 | }; 93 | 94 | websocket.connection.onclose = function() { 95 | websocket.connection = null; 96 | } 97 | } 98 | 99 | }; 100 | 101 | /** 102 | * @description: 103 | * Check if websockets are supported 104 | * If so, call establishWebsocketConnection() 105 | * 106 | * @return Null 107 | */ 108 | this.checkIfWebsocketsAreSupported = function() { 109 | 110 | var websocketSupport = { 111 | browser: null, 112 | server: null, 113 | }; 114 | 115 | // does browser support websockets? 116 | if (window.WebSocket) { 117 | 118 | websocketSupport.browser = true; 119 | 120 | // does backend support websockets? 121 | $http.get("/websocket").then(function(response) { 122 | 123 | // if websocket_support property exists and is trurthy 124 | // websocketSupport.server will equal true. 125 | websocketSupport.server = !!response.data["websocket_support"]; 126 | 127 | }).catch(function websocketNotSupportedByServer() { 128 | 129 | websocketSupport.server = false; 130 | $rootScope.$broadcast("start-plex-dash", {}); 131 | 132 | }).then(function finalDecisionOnWebsocket() { 133 | 134 | if (websocketSupport.browser && websocketSupport.server) { 135 | 136 | establishWebsocketConnection(); 137 | 138 | } else { 139 | // rootScope event not propogating from here. 140 | // instead, we manually route to url 141 | $location.path('/system-status'); 142 | } 143 | 144 | }); 145 | 146 | } 147 | 148 | }; 149 | 150 | /** 151 | * Handles requests from modules for data from server 152 | * 153 | * @param {String} moduleName 154 | * @param {Function} callback 155 | * @return {[ Null || callback(server response) ]} 156 | */ 157 | this.get = function(moduleName, callback) { 158 | 159 | // if we have a websocket connection 160 | if (websocket.connection) { 161 | 162 | // and the connection is ready 163 | if (websocket.connection.readyState === 1) { 164 | 165 | // set the callback as the event handler 166 | // for server response. 167 | // 168 | // Callback instance needs to be overwritten 169 | // each time for this to work. Not sure why. 170 | websocket.onMessageEventHandlers[moduleName] = callback; 171 | 172 | // 173 | websocket.connection.send(moduleName); 174 | 175 | } else { 176 | console.log("Websocket not ready yet.", moduleName); 177 | } 178 | 179 | } 180 | // otherwise 181 | else { 182 | 183 | var moduleAddress = 'main_files/?module=' + moduleName; 184 | 185 | return $http.get(moduleAddress).then(function(response) { 186 | return callback(response.data); 187 | }); 188 | 189 | } 190 | 191 | }; 192 | 193 | }]); 194 | 195 | /** 196 | * Hook to run websocket support check. 197 | */ 198 | angular.module('plexDash').run(function(server, $location, $rootScope) { 199 | 200 | server.checkIfWebsocketsAreSupported(); 201 | 202 | var currentRoute = $location.path(); 203 | var currentTab = (currentRoute === '/loading')? 'system-status': currentRoute; 204 | localStorage.setItem('currentTab', currentTab); 205 | 206 | $location.path('/loading'); 207 | 208 | }); 209 | 210 | /** 211 | * Sidebar for SPA 212 | */ 213 | angular.module('plexDash').directive('navBar', function($location) { 214 | return { 215 | restrict: 'E', 216 | templateUrl: 'main_files/app/navbar.html', 217 | link: function(scope) { 218 | scope.items = [ 219 | 'system-status', 220 | 'basic-info', 221 | 'network', 222 | 'accounts', 223 | 'apps' 224 | ]; 225 | 226 | scope.getNavItemName = function(url) { 227 | return url.replace('-', ' '); 228 | }; 229 | 230 | scope.isActive = function(route) { 231 | return '/' + route === $location.path(); 232 | }; 233 | } 234 | }; 235 | 236 | }); 237 | 238 | ////////////////////////////////////////////////////////////// 239 | ////////////////// UI Element Directives ////////////////// // 240 | ////////////////////////////////////////////////////////////// 241 | 242 | /** 243 | * Shows loader 244 | */ 245 | angular.module('plexDash').directive('loader', function() { 246 | return { 247 | restrict: 'E', 248 | scope: { 249 | width: '@' 250 | }, 251 | template: '
' + 252 | '
' + 253 | '
' + 254 | '
' + 255 | '
' + 256 | '
' + 257 | '
' 258 | }; 259 | }); 260 | 261 | /** 262 | * Top Bar for widget 263 | */ 264 | angular.module('plexDash').directive('topBar', function() { 265 | return { 266 | restrict: 'E', 267 | scope: { 268 | heading: '=', 269 | refresh: '&', 270 | lastUpdated: '=', 271 | info: '=', 272 | }, 273 | templateUrl: 'main_files/app/ui-elements/top-bar.html', 274 | link: function(scope, element, attrs) { 275 | var $refreshBtn = element.find('refresh-btn').eq(0); 276 | 277 | if (typeof attrs.noRefreshBtn !== 'undefined') { 278 | $refreshBtn.remove(); 279 | } 280 | } 281 | }; 282 | }); 283 | 284 | /** 285 | * Shows refresh button and calls 286 | * provided expression on-click 287 | */ 288 | angular.module('plexDash').directive('refreshBtn', function() { 289 | return { 290 | restrict: 'E', 291 | scope: { 292 | refresh: '&' 293 | }, 294 | template: '' 295 | }; 296 | }); 297 | 298 | /** 299 | * Message shown when no data is found from server 300 | */ 301 | angular.module('plexDash').directive('noData', function() { 302 | return { 303 | restrict: 'E', 304 | template: 'No Data' 305 | }; 306 | }); 307 | 308 | /** 309 | * Displays last updated timestamp for widget 310 | */ 311 | angular.module('plexDash').directive('lastUpdate', function() { 312 | return { 313 | restrict: 'E', 314 | scope: { 315 | timestamp: '=' 316 | }, 317 | templateUrl: 'main_files/app/ui-elements/last-update.html' 318 | }; 319 | }); 320 | 321 | 322 | ////////////////// Plugin Directives ////////////////// 323 | 324 | /** 325 | * Fetches and displays table data 326 | */ 327 | angular.module('plexDash').directive('tableData', ['server', '$rootScope', function(server, $rootScope) { 328 | return { 329 | restrict: 'E', 330 | scope: { 331 | heading: '@', 332 | info: '@', 333 | moduleName: '@' 334 | }, 335 | templateUrl: 'main_files/app/table-data-plugin.html', 336 | link: function(scope, element) { 337 | 338 | scope.sortByColumn = null; 339 | scope.sortReverse = null; 340 | 341 | // set the column to sort by 342 | scope.setSortColumn = function(column) { 343 | 344 | // if the column is already being sorted 345 | // reverse the order 346 | if (column === scope.sortByColumn) { 347 | scope.sortReverse = !scope.sortReverse; 348 | } else { 349 | scope.sortByColumn = column; 350 | } 351 | 352 | scope.sortTableRows(); 353 | }; 354 | 355 | scope.sortTableRows = function() { 356 | scope.tableRows.sort(function(currentRow, nextRow) { 357 | 358 | var sortResult = 0; 359 | 360 | if (currentRow[scope.sortByColumn] < nextRow[scope.sortByColumn]) { 361 | sortResult = -1; 362 | } else if (currentRow[scope.sortByColumn] === nextRow[scope.sortByColumn]) { 363 | sortResult = 0; 364 | } else { 365 | sortResult = 1; 366 | } 367 | 368 | if (scope.sortReverse) { 369 | sortResult = -1 * sortResult; 370 | } 371 | 372 | return sortResult; 373 | }); 374 | }; 375 | 376 | scope.getData = function() { 377 | delete scope.tableRows; 378 | 379 | server.get(scope.moduleName, function(serverResponseData) { 380 | 381 | if (serverResponseData.length > 0) { 382 | scope.tableHeaders = Object.keys(serverResponseData[0]); 383 | } 384 | 385 | scope.tableRows = serverResponseData; 386 | 387 | if (scope.sortByColumn) { 388 | scope.sortTableRows(); 389 | } 390 | 391 | scope.lastGet = new Date().getTime(); 392 | 393 | if (serverResponseData.length < 1) { 394 | scope.emptyResult = true; 395 | } 396 | 397 | if (!scope.$$phase && !$rootScope.$$phase) scope.$digest(); 398 | }); 399 | }; 400 | 401 | scope.getData(); 402 | } 403 | }; 404 | }]); 405 | 406 | /** 407 | * Fetches and displays table data 408 | */ 409 | angular.module('plexDash').directive('keyValueList', ['server', '$rootScope', function(server, $rootScope) { 410 | return { 411 | restrict: 'E', 412 | scope: { 413 | heading: '@', 414 | info: '@', 415 | moduleName: '@', 416 | }, 417 | templateUrl: 'main_files/app/key-value-list-plugin.html', 418 | link: function(scope, element) { 419 | 420 | scope.getData = function() { 421 | delete scope.tableRows; 422 | 423 | server.get(scope.moduleName, function(serverResponseData) { 424 | scope.tableRows = serverResponseData; 425 | scope.lastGet = new Date().getTime(); 426 | 427 | if (Object.keys(serverResponseData).length === 0) { 428 | scope.emptyResult = true; 429 | } 430 | 431 | if (!scope.$$phase && !$rootScope.$$phase) scope.$digest(); 432 | }); 433 | }; 434 | 435 | scope.getData(); 436 | } 437 | }; 438 | }]); 439 | 440 | /** 441 | * Fetches and displays data as line chart at a certain refresh rate 442 | */ 443 | angular.module('plexDash').directive('lineChartPlugin', ['$interval', '$compile', 'server', '$window', function($interval, $compile, server, $window) { 444 | return { 445 | restrict: 'E', 446 | scope: { 447 | heading: '@', 448 | moduleName: '@', 449 | refreshRate: '=', 450 | maxValue: '=', 451 | minValue: '=', 452 | getDisplayValue: '=', 453 | metrics: '=', 454 | color: '@' 455 | }, 456 | templateUrl: 'main_files/app/line-chart-plugin.html', 457 | link: function(scope, element) { 458 | 459 | if (!scope.color) scope.color = '0, 255, 0'; 460 | 461 | var series, w, h, canvas; 462 | 463 | angular.element($window).bind('resize', function() { 464 | canvas.width = w; 465 | canvas.height = h; 466 | }); 467 | 468 | // smoothieJS - Create new chart 469 | var chart = new SmoothieChart({ 470 | borderVisible: false, 471 | sharpLines: true, 472 | grid: { 473 | fillStyle: '#ffffff', 474 | strokeStyle: 'rgba(232,230,230,0.93)', 475 | sharpLines: true, 476 | millisPerLine: 3000, 477 | borderVisible: false 478 | }, 479 | labels: { 480 | fontSize: 11, 481 | precision: 0, 482 | fillStyle: '#0f0e0e' 483 | }, 484 | maxValue: parseInt(scope.maxValue), 485 | minValue: parseInt(scope.minValue), 486 | horizontalLines: [{ 487 | value: 5, 488 | color: '#eff', 489 | lineWidth: 1 490 | }] 491 | }); 492 | 493 | // smoothieJS - set up canvas element for chart 494 | canvas = element.find('canvas')[0]; 495 | series = new TimeSeries(); 496 | w = canvas.width; 497 | h = canvas.height; 498 | 499 | chart.addTimeSeries(series, { 500 | strokeStyle: 'rgba(' + scope.color + ', 1)', 501 | fillStyle: 'rgba(' + scope.color + ', 0.2)', 502 | lineWidth: 2 503 | }); 504 | 505 | chart.streamTo(canvas, 1000); 506 | 507 | var dataCallInProgress = false; 508 | 509 | // update data on chart 510 | scope.getData = function() { 511 | 512 | if (dataCallInProgress) return; 513 | 514 | dataCallInProgress = true; 515 | 516 | server.get(scope.moduleName, function(serverResponseData) { 517 | 518 | dataCallInProgress = false; 519 | scope.lastGet = new Date().getTime(); 520 | 521 | // change graph colour depending on usage 522 | if (scope.maxValue / 4 * 3 < scope.getDisplayValue(serverResponseData)) { 523 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 89, 0, 1)'; 524 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 89, 0, 0.2)'; 525 | } else if (scope.maxValue / 3 < scope.getDisplayValue(serverResponseData)) { 526 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 238, 0, 1)'; 527 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 238, 0, 0.2)'; 528 | } else { 529 | chart.seriesSet[0].options.strokeStyle = 'rgba(' + scope.color + ', 1)'; 530 | chart.seriesSet[0].options.fillStyle = 'rgba(' + scope.color + ', 0.2)'; 531 | } 532 | 533 | // update chart with this response 534 | series.append(scope.lastGet, scope.getDisplayValue(serverResponseData)); 535 | 536 | // update the metrics for this chart 537 | scope.metrics.forEach(function(metricObj) { 538 | metricObj.data = metricObj.generate(serverResponseData); 539 | }); 540 | 541 | }); 542 | }; 543 | 544 | // set the directive-provided interval 545 | // at which to run the chart update 546 | var intervalRef = $interval(scope.getData, scope.refreshRate); 547 | var removeInterval = function() { 548 | $interval.cancel(intervalRef); 549 | }; 550 | 551 | element.on("$destroy", removeInterval); 552 | } 553 | }; 554 | }]); 555 | 556 | /** 557 | * Fetches and displays data as line chart at a certain refresh rate 558 | * 559 | */ 560 | angular.module('plexDash').directive('multiLineChartPlugin', ['$interval', '$compile', 'server', '$window', function($interval, $compile, server, $window) { 561 | return { 562 | restrict: 'E', 563 | scope: { 564 | heading: '@', 565 | moduleName: '@', 566 | refreshRate: '=', 567 | getDisplayValue: '=', 568 | units: '=', 569 | delay: '=' 570 | }, 571 | templateUrl: 'main_files/app/multi-line-chart-plugin.html', 572 | link: function(scope, element) { 573 | 574 | var w, h, canvas; 575 | 576 | angular.element($window).bind('resize', function() { 577 | canvas.width = w; 578 | canvas.height = h; 579 | }); 580 | 581 | // smoothieJS - Create new chart 582 | var chart = new SmoothieChart({ 583 | borderVisible: false, 584 | sharpLines: true, 585 | grid: { 586 | fillStyle: '#ffffff', 587 | strokeStyle: 'rgba(232,230,230,0.93)', 588 | sharpLines: true, 589 | borderVisible: false 590 | }, 591 | labels: { 592 | fontSize: 12, 593 | precision: 0, 594 | fillStyle: '#0f0e0e' 595 | }, 596 | maxValue: 100, 597 | minValue: 0, 598 | horizontalLines: [{ 599 | value: 1, 600 | color: '#ecc', 601 | lineWidth: 1 602 | }] 603 | }); 604 | 605 | var seriesOptions = [{ 606 | strokeStyle: 'rgba(255, 0, 0, 1)', 607 | lineWidth: 2 608 | }, { 609 | strokeStyle: 'rgba(0, 255, 0, 1)', 610 | lineWidth: 2 611 | }, { 612 | strokeStyle: 'rgba(0, 0, 255, 1)', 613 | lineWidth: 2 614 | }, { 615 | strokeStyle: 'rgba(255, 255, 0, 1)', 616 | lineWidth: 1 617 | }]; 618 | 619 | // smoothieJS - set up canvas element for chart 620 | var canvas = element.find('canvas')[0]; 621 | w = canvas.width; 622 | h = canvas.height; 623 | scope.seriesArray = []; 624 | scope.metricsArray = []; 625 | 626 | // get the data once to set up # of lines on chart 627 | server.get(scope.moduleName, function(serverResponseData) { 628 | 629 | var numberOfLines = Object.keys(serverResponseData).length; 630 | 631 | for (var x = 0; x < numberOfLines; x++) { 632 | 633 | var keyForThisLine = Object.keys(serverResponseData)[x]; 634 | 635 | scope.seriesArray[x] = new TimeSeries(); 636 | chart.addTimeSeries(scope.seriesArray[x], seriesOptions[x]); 637 | scope.metricsArray[x] = { 638 | name: keyForThisLine, 639 | color: seriesOptions[x].strokeStyle, 640 | }; 641 | } 642 | 643 | }); 644 | 645 | var delay = 1000; 646 | 647 | if (angular.isDefined(scope.delay)) 648 | delay = scope.delay; 649 | 650 | chart.streamTo(canvas, delay); 651 | 652 | var dataCallInProgress = false; 653 | 654 | // update data on chart 655 | scope.getData = function() { 656 | 657 | if (dataCallInProgress) return; 658 | 659 | if (!scope.seriesArray.length) return; 660 | 661 | dataCallInProgress = true; 662 | 663 | server.get(scope.moduleName, function(serverResponseData) { 664 | 665 | dataCallInProgress = false; 666 | scope.lastGet = new Date().getTime(); 667 | var keyCount = 0; 668 | var maxAvg = 100; 669 | 670 | // update chart with current response 671 | for (var key in serverResponseData) { 672 | scope.seriesArray[keyCount].append(scope.lastGet, serverResponseData[key]); 673 | keyCount++; 674 | maxAvg = Math.max(maxAvg, serverResponseData[key]); 675 | } 676 | 677 | // update the metrics for this chart 678 | scope.metricsArray.forEach(function(metricObj) { 679 | metricObj.data = serverResponseData[metricObj.name].toString() + ' ' + scope.units; 680 | }); 681 | 682 | // round up the average and set the maximum scale 683 | var len = parseInt(Math.log(maxAvg) / Math.log(10)); 684 | var div = Math.pow(10, len); 685 | chart.options.maxValue = Math.ceil(maxAvg / div) * div; 686 | 687 | }); 688 | 689 | }; 690 | 691 | var refreshRate = (angular.isDefined(scope.refreshRate)) ? scope.refreshRate : 1000; 692 | var intervalRef = $interval(scope.getData, refreshRate); 693 | var removeInterval = function() { 694 | $interval.cancel(intervalRef); 695 | }; 696 | 697 | element.on("$destroy", removeInterval); 698 | } 699 | }; 700 | }]); 701 | 702 | /** 703 | * Base plugin structure 704 | */ 705 | angular.module('plexDash').directive('plugin', function() { 706 | return { 707 | restrict: 'E', 708 | transclude: true, 709 | templateUrl: 'main_files/app/base-plugin.html' 710 | } 711 | }); 712 | 713 | /** 714 | * Progress bar element 715 | */ 716 | angular.module('plexDash').directive('progressBarPlugin', function() { 717 | return { 718 | restrict: 'E', 719 | scope: { 720 | width: '@', 721 | moduleName: '@', 722 | name: '@', 723 | value: '@', 724 | max: '@' 725 | }, 726 | templateUrl: 'main_files/app/progress-bar-plugin.html' 727 | }; 728 | }); 729 | }()); 730 | -------------------------------------------------------------------------------- /main_files/modules/python_files/speedtest_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2012-2014 Matt Martz 4 | # All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | __version__ = '0.3.1' 19 | 20 | # Some global variables we use 21 | source = None 22 | shutdown_event = None 23 | 24 | import os 25 | import re 26 | import sys 27 | import math 28 | import signal 29 | import socket 30 | import timeit 31 | import threading 32 | 33 | # Used for bound_interface 34 | socket_socket = socket.socket 35 | 36 | try: 37 | import xml.etree.cElementTree as ET 38 | except ImportError: 39 | try: 40 | import xml.etree.ElementTree as ET 41 | except ImportError: 42 | from xml.dom import minidom as DOM 43 | ET = None 44 | 45 | # Begin import game to handle Python 2 and Python 3 46 | try: 47 | from urllib2 import urlopen, Request, HTTPError, URLError 48 | except ImportError: 49 | from urllib.request import urlopen, Request, HTTPError, URLError 50 | 51 | try: 52 | from httplib import HTTPConnection, HTTPSConnection 53 | except ImportError: 54 | from http.client import HTTPConnection, HTTPSConnection 55 | 56 | try: 57 | from Queue import Queue 58 | except ImportError: 59 | from queue import Queue 60 | 61 | try: 62 | from urlparse import urlparse 63 | except ImportError: 64 | from urllib.parse import urlparse 65 | 66 | try: 67 | from urlparse import parse_qs 68 | except ImportError: 69 | try: 70 | from urllib.parse import parse_qs 71 | except ImportError: 72 | from cgi import parse_qs 73 | 74 | try: 75 | from hashlib import md5 76 | except ImportError: 77 | from md5 import md5 78 | 79 | try: 80 | from argparse import ArgumentParser as ArgParser 81 | except ImportError: 82 | from optparse import OptionParser as ArgParser 83 | 84 | try: 85 | import builtins 86 | except ImportError: 87 | def print_(*args, **kwargs): 88 | """The new-style print function taken from 89 | https://pypi.python.org/pypi/six/ 90 | 91 | """ 92 | fp = kwargs.pop("file", sys.stdout) 93 | if fp is None: 94 | return 95 | 96 | def write(data): 97 | if not isinstance(data, basestring): 98 | data = str(data) 99 | fp.write(data) 100 | 101 | want_unicode = False 102 | sep = kwargs.pop("sep", None) 103 | if sep is not None: 104 | if isinstance(sep, unicode): 105 | want_unicode = True 106 | elif not isinstance(sep, str): 107 | raise TypeError("sep must be None or a string") 108 | end = kwargs.pop("end", None) 109 | if end is not None: 110 | if isinstance(end, unicode): 111 | want_unicode = True 112 | elif not isinstance(end, str): 113 | raise TypeError("end must be None or a string") 114 | if kwargs: 115 | raise TypeError("invalid keyword arguments to print()") 116 | if not want_unicode: 117 | for arg in args: 118 | if isinstance(arg, unicode): 119 | want_unicode = True 120 | break 121 | if want_unicode: 122 | newline = unicode("\n") 123 | space = unicode(" ") 124 | else: 125 | newline = "\n" 126 | space = " " 127 | if sep is None: 128 | sep = space 129 | if end is None: 130 | end = newline 131 | for i, arg in enumerate(args): 132 | if i: 133 | write(sep) 134 | write(arg) 135 | write(end) 136 | else: 137 | print_ = getattr(builtins, 'print') 138 | del builtins 139 | 140 | 141 | def bound_socket(*args, **kwargs): 142 | """Bind socket to a specified source IP address""" 143 | 144 | global source 145 | sock = socket_socket(*args, **kwargs) 146 | sock.bind((source, 0)) 147 | return sock 148 | 149 | 150 | def distance(origin, destination): 151 | """Determine distance between 2 sets of [lat,lon] in km""" 152 | 153 | lat1, lon1 = origin 154 | lat2, lon2 = destination 155 | radius = 6371 # km 156 | 157 | dlat = math.radians(lat2 - lat1) 158 | dlon = math.radians(lon2 - lon1) 159 | a = (math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos(math.radians(lat1)) 160 | * math.cos(math.radians(lat2)) * math.sin(dlon / 2) 161 | * math.sin(dlon / 2)) 162 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 163 | d = radius * c 164 | 165 | return d 166 | 167 | 168 | class FileGetter(threading.Thread): 169 | """Thread class for retrieving a URL""" 170 | 171 | def __init__(self, url, start): 172 | self.url = url 173 | self.result = None 174 | self.starttime = start 175 | threading.Thread.__init__(self) 176 | 177 | def run(self): 178 | self.result = [0] 179 | try: 180 | if (timeit.default_timer() - self.starttime) <= 10: 181 | f = urlopen(self.url) 182 | while 1 and not shutdown_event.isSet(): 183 | self.result.append(len(f.read(10240))) 184 | if self.result[-1] == 0: 185 | break 186 | f.close() 187 | except IOError: 188 | pass 189 | 190 | 191 | def downloadSpeed(files, quiet=False): 192 | """Function to launch FileGetter threads and calculate download speeds""" 193 | 194 | start = timeit.default_timer() 195 | 196 | def producer(q, files): 197 | for file in files: 198 | thread = FileGetter(file, start) 199 | thread.start() 200 | q.put(thread, True) 201 | if not quiet and not shutdown_event.isSet(): 202 | sys.stdout.write('.') 203 | sys.stdout.flush() 204 | 205 | finished = [] 206 | 207 | def consumer(q, total_files): 208 | while len(finished) < total_files: 209 | thread = q.get(True) 210 | while thread.isAlive(): 211 | thread.join(timeout=0.1) 212 | finished.append(sum(thread.result)) 213 | del thread 214 | 215 | q = Queue(6) 216 | prod_thread = threading.Thread(target=producer, args=(q, files)) 217 | cons_thread = threading.Thread(target=consumer, args=(q, len(files))) 218 | start = timeit.default_timer() 219 | prod_thread.start() 220 | cons_thread.start() 221 | while prod_thread.isAlive(): 222 | prod_thread.join(timeout=0.1) 223 | while cons_thread.isAlive(): 224 | cons_thread.join(timeout=0.1) 225 | return (sum(finished) / (timeit.default_timer() - start)) 226 | 227 | 228 | class FilePutter(threading.Thread): 229 | """Thread class for putting a URL""" 230 | 231 | def __init__(self, url, start, size): 232 | self.url = url 233 | chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 234 | data = chars * (int(round(int(size) / 36.0))) 235 | self.data = ('content1=%s' % data[0:int(size) - 9]).encode() 236 | del data 237 | self.result = None 238 | self.starttime = start 239 | threading.Thread.__init__(self) 240 | 241 | def run(self): 242 | try: 243 | if ((timeit.default_timer() - self.starttime) <= 10 and 244 | not shutdown_event.isSet()): 245 | f = urlopen(self.url, self.data) 246 | f.read(11) 247 | f.close() 248 | self.result = len(self.data) 249 | else: 250 | self.result = 0 251 | except IOError: 252 | self.result = 0 253 | 254 | 255 | def uploadSpeed(url, sizes, quiet=False): 256 | """Function to launch FilePutter threads and calculate upload speeds""" 257 | 258 | start = timeit.default_timer() 259 | 260 | def producer(q, sizes): 261 | for size in sizes: 262 | thread = FilePutter(url, start, size) 263 | thread.start() 264 | q.put(thread, True) 265 | if not quiet and not shutdown_event.isSet(): 266 | sys.stdout.write('.') 267 | sys.stdout.flush() 268 | 269 | finished = [] 270 | 271 | def consumer(q, total_sizes): 272 | while len(finished) < total_sizes: 273 | thread = q.get(True) 274 | while thread.isAlive(): 275 | thread.join(timeout=0.1) 276 | finished.append(thread.result) 277 | del thread 278 | 279 | q = Queue(6) 280 | prod_thread = threading.Thread(target=producer, args=(q, sizes)) 281 | cons_thread = threading.Thread(target=consumer, args=(q, len(sizes))) 282 | start = timeit.default_timer() 283 | prod_thread.start() 284 | cons_thread.start() 285 | while prod_thread.isAlive(): 286 | prod_thread.join(timeout=0.1) 287 | while cons_thread.isAlive(): 288 | cons_thread.join(timeout=0.1) 289 | return (sum(finished) / (timeit.default_timer() - start)) 290 | 291 | 292 | def getAttributesByTagName(dom, tagName): 293 | """Retrieve an attribute from an XML document and return it in a 294 | consistent format 295 | 296 | Only used with xml.dom.minidom, which is likely only to be used 297 | with python versions older than 2.5 298 | """ 299 | elem = dom.getElementsByTagName(tagName)[0] 300 | return dict(list(elem.attributes.items())) 301 | 302 | 303 | def getConfig(): 304 | """Download the speedtest.net configuration and return only the data 305 | we are interested in 306 | """ 307 | 308 | uh = urlopen('http://www.speedtest.net/speedtest-config.php') 309 | configxml = [] 310 | while 1: 311 | configxml.append(uh.read(10240)) 312 | if len(configxml[-1]) == 0: 313 | break 314 | if int(uh.code) != 200: 315 | return None 316 | uh.close() 317 | try: 318 | try: 319 | root = ET.fromstring(''.encode().join(configxml)) 320 | config = { 321 | 'client': root.find('client').attrib, 322 | 'times': root.find('times').attrib, 323 | 'download': root.find('download').attrib, 324 | 'upload': root.find('upload').attrib} 325 | except AttributeError: 326 | root = DOM.parseString(''.join(configxml)) 327 | config = { 328 | 'client': getAttributesByTagName(root, 'client'), 329 | 'times': getAttributesByTagName(root, 'times'), 330 | 'download': getAttributesByTagName(root, 'download'), 331 | 'upload': getAttributesByTagName(root, 'upload')} 332 | except SyntaxError: 333 | print_('Failed to parse speedtest.net configuration') 334 | sys.exit(1) 335 | del root 336 | del configxml 337 | return config 338 | 339 | 340 | def closestServers(client, all=False): 341 | """Determine the 5 closest speedtest.net servers based on geographic 342 | distance 343 | """ 344 | 345 | uh = urlopen('http://www.speedtest.net/speedtest-servers-static.php') 346 | serversxml = [] 347 | while 1: 348 | serversxml.append(uh.read(10240)) 349 | if len(serversxml[-1]) == 0: 350 | break 351 | if int(uh.code) != 200: 352 | return None 353 | uh.close() 354 | try: 355 | try: 356 | root = ET.fromstring(''.encode().join(serversxml)) 357 | elements = root.getiterator('server') 358 | except AttributeError: 359 | root = DOM.parseString(''.join(serversxml)) 360 | elements = root.getElementsByTagName('server') 361 | except SyntaxError: 362 | print_('Failed to parse list of speedtest.net servers') 363 | sys.exit(1) 364 | servers = {} 365 | for server in elements: 366 | try: 367 | attrib = server.attrib 368 | except AttributeError: 369 | attrib = dict(list(server.attributes.items())) 370 | d = distance([float(client['lat']), float(client['lon'])], 371 | [float(attrib.get('lat')), float(attrib.get('lon'))]) 372 | attrib['d'] = d 373 | if d not in servers: 374 | servers[d] = [attrib] 375 | else: 376 | servers[d].append(attrib) 377 | del root 378 | del serversxml 379 | del elements 380 | 381 | closest = [] 382 | for d in sorted(servers.keys()): 383 | for s in servers[d]: 384 | closest.append(s) 385 | if len(closest) == 5 and not all: 386 | break 387 | else: 388 | continue 389 | break 390 | 391 | del servers 392 | return closest 393 | 394 | 395 | def getBestServer(servers): 396 | """Perform a speedtest.net latency request to determine which 397 | speedtest.net server has the lowest latency 398 | """ 399 | 400 | results = {} 401 | for server in servers: 402 | cum = [] 403 | url = '%s/latency.txt' % os.path.dirname(server['url']) 404 | urlparts = urlparse(url) 405 | for i in range(0, 3): 406 | try: 407 | if urlparts[0] == 'https': 408 | h = HTTPSConnection(urlparts[1]) 409 | else: 410 | h = HTTPConnection(urlparts[1]) 411 | start = timeit.default_timer() 412 | h.request("GET", urlparts[2]) 413 | r = h.getresponse() 414 | total = (timeit.default_timer() - start) 415 | except (HTTPError, URLError, socket.error): 416 | cum.append(3600) 417 | continue 418 | text = r.read(9) 419 | if int(r.status) == 200 and text == 'test=test'.encode(): 420 | cum.append(total) 421 | else: 422 | cum.append(3600) 423 | h.close() 424 | avg = round((sum(cum) / 6) * 1000, 3) 425 | results[avg] = server 426 | fastest = sorted(results.keys())[0] 427 | best = results[fastest] 428 | best['latency'] = fastest 429 | 430 | return best 431 | 432 | 433 | def ctrl_c(signum, frame): 434 | """Catch Ctrl-C key sequence and set a shutdown_event for our threaded 435 | operations 436 | """ 437 | 438 | global shutdown_event 439 | shutdown_event.set() 440 | raise SystemExit('\nCancelling...') 441 | 442 | 443 | def version(): 444 | """Print the version""" 445 | 446 | raise SystemExit(__version__) 447 | 448 | 449 | def speedtest(): 450 | """Run the full speedtest.net test""" 451 | 452 | global shutdown_event, source 453 | shutdown_event = threading.Event() 454 | 455 | signal.signal(signal.SIGINT, ctrl_c) 456 | 457 | description = ( 458 | 'Command line interface for testing internet bandwidth using ' 459 | 'speedtest.net.\n' 460 | '------------------------------------------------------------' 461 | '--------------\n' 462 | 'https://github.com/sivel/speedtest-cli') 463 | 464 | parser = ArgParser(description=description) 465 | # Give optparse.OptionParser an `add_argument` method for 466 | # compatibility with argparse.ArgumentParser 467 | try: 468 | parser.add_argument = parser.add_option 469 | except AttributeError: 470 | pass 471 | parser.add_argument('--bytes', dest='units', action='store_const', 472 | const=('bytes', 1), default=('bits', 8), 473 | help='Display values in bytes instead of bits. Does ' 474 | 'not affect the image generated by --share') 475 | parser.add_argument('--share', action='store_true', 476 | help='Generate and provide a URL to the speedtest.net ' 477 | 'share results image') 478 | parser.add_argument('--simple', action='store_true', 479 | help='Suppress verbose output, only show basic ' 480 | 'information') 481 | parser.add_argument('--list', action='store_true', 482 | help='Display a list of speedtest.net servers ' 483 | 'sorted by distance') 484 | parser.add_argument('--server', help='Specify a server ID to test against') 485 | parser.add_argument('--mini', help='URL of the Speedtest Mini server') 486 | parser.add_argument('--source', help='Source IP address to bind to') 487 | parser.add_argument('--version', action='store_true', 488 | help='Show the version number and exit') 489 | 490 | options = parser.parse_args() 491 | if isinstance(options, tuple): 492 | args = options[0] 493 | else: 494 | args = options 495 | del options 496 | 497 | # Print the version and exit 498 | if args.version: 499 | version() 500 | 501 | # If specified bind to a specific IP address 502 | if args.source: 503 | source = args.source 504 | socket.socket = bound_socket 505 | 506 | if not args.simple: 507 | print_('Retrieving speedtest.net configuration...') 508 | try: 509 | config = getConfig() 510 | except URLError: 511 | print_('Cannot retrieve speedtest configuration') 512 | sys.exit(1) 513 | 514 | if not args.simple: 515 | print_('Retrieving speedtest.net server list...') 516 | if args.list or args.server: 517 | servers = closestServers(config['client'], True) 518 | if args.list: 519 | serverList = [] 520 | for server in servers: 521 | line = ('%(id)4s) %(sponsor)s (%(name)s, %(country)s) ' 522 | '[%(d)0.2f km]' % server) 523 | serverList.append(line) 524 | # Python 2.7 and newer seem to be ok with the resultant encoding 525 | # from parsing the XML, but older versions have some issues. 526 | # This block should detect whether we need to encode or not 527 | try: 528 | unicode() 529 | print_('\n'.join(serverList).encode('utf-8', 'ignore')) 530 | except NameError: 531 | print_('\n'.join(serverList)) 532 | except IOError: 533 | pass 534 | sys.exit(0) 535 | else: 536 | servers = closestServers(config['client']) 537 | 538 | if not args.simple: 539 | print_('Testing from %(isp)s (%(ip)s)...' % config['client']) 540 | 541 | if args.server: 542 | try: 543 | best = getBestServer(filter(lambda x: x['id'] == args.server, 544 | servers)) 545 | except IndexError: 546 | print_('Invalid server ID') 547 | sys.exit(1) 548 | elif args.mini: 549 | name, ext = os.path.splitext(args.mini) 550 | if ext: 551 | url = os.path.dirname(args.mini) 552 | else: 553 | url = args.mini 554 | urlparts = urlparse(url) 555 | try: 556 | f = urlopen(args.mini) 557 | except: 558 | print_('Invalid Speedtest Mini URL') 559 | sys.exit(1) 560 | else: 561 | text = f.read() 562 | f.close() 563 | extension = re.findall('upload_extension: "([^"]+)"', text.decode()) 564 | if not extension: 565 | for ext in ['php', 'asp', 'aspx', 'jsp']: 566 | try: 567 | f = urlopen('%s/speedtest/upload.%s' % (args.mini, ext)) 568 | except: 569 | pass 570 | else: 571 | data = f.read().strip() 572 | if (f.code == 200 and 573 | len(data.splitlines()) == 1 and 574 | re.match('size=[0-9]', data)): 575 | extension = [ext] 576 | break 577 | if not urlparts or not extension: 578 | print_('Please provide the full URL of your Speedtest Mini server') 579 | sys.exit(1) 580 | servers = [{ 581 | 'sponsor': 'Speedtest Mini', 582 | 'name': urlparts[1], 583 | 'd': 0, 584 | 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]), 585 | 'latency': 0, 586 | 'id': 0 587 | }] 588 | try: 589 | best = getBestServer(servers) 590 | except: 591 | best = servers[0] 592 | else: 593 | if not args.simple: 594 | print_('Selecting best server based on latency...') 595 | best = getBestServer(servers) 596 | 597 | if not args.simple: 598 | # Python 2.7 and newer seem to be ok with the resultant encoding 599 | # from parsing the XML, but older versions have some issues. 600 | # This block should detect whether we need to encode or not 601 | try: 602 | unicode() 603 | print_(('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' 604 | '%(latency)s ms' % best).encode('utf-8', 'ignore')) 605 | except NameError: 606 | print_('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' 607 | '%(latency)s ms' % best) 608 | else: 609 | print_('Ping: %(latency)s ms' % best) 610 | 611 | sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000] 612 | urls = [] 613 | for size in sizes: 614 | for i in range(0, 4): 615 | urls.append('%s/random%sx%s.jpg' % 616 | (os.path.dirname(best['url']), size, size)) 617 | if not args.simple: 618 | print_('Testing download speed', end='') 619 | dlspeed = downloadSpeed(urls, args.simple) 620 | if not args.simple: 621 | print_() 622 | print_('Download: %0.2f M%s/s' % 623 | ((dlspeed / 1000 / 1000) * args.units[1], args.units[0])) 624 | 625 | sizesizes = [int(.25 * 1000 * 1000), int(.5 * 1000 * 1000)] 626 | sizes = [] 627 | for size in sizesizes: 628 | for i in range(0, 25): 629 | sizes.append(size) 630 | if not args.simple: 631 | print_('Testing upload speed', end='') 632 | ulspeed = uploadSpeed(best['url'], sizes, args.simple) 633 | if not args.simple: 634 | print_() 635 | print_('Upload: %0.2f M%s/s' % 636 | ((ulspeed / 1000 / 1000) * args.units[1], args.units[0])) 637 | 638 | if args.share and args.mini: 639 | print_('Cannot generate a speedtest.net share results image while ' 640 | 'testing against a Speedtest Mini server') 641 | elif args.share: 642 | dlspeedk = int(round((dlspeed / 1000) * 8, 0)) 643 | ping = int(round(best['latency'], 0)) 644 | ulspeedk = int(round((ulspeed / 1000) * 8, 0)) 645 | 646 | # Build the request to send results back to speedtest.net 647 | # We use a list instead of a dict because the API expects parameters 648 | # in a certain order 649 | apiData = [ 650 | 'download=%s' % dlspeedk, 651 | 'ping=%s' % ping, 652 | 'upload=%s' % ulspeedk, 653 | 'promo=', 654 | 'startmode=%s' % 'pingselect', 655 | 'recommendedserverid=%s' % best['id'], 656 | 'accuracy=%s' % 1, 657 | 'serverid=%s' % best['id'], 658 | 'hash=%s' % md5(('%s-%s-%s-%s' % 659 | (ping, ulspeedk, dlspeedk, '297aae72')) 660 | .encode()).hexdigest()] 661 | 662 | req = Request('http://www.speedtest.net/api/api.php', 663 | data='&'.join(apiData).encode()) 664 | req.add_header('Referer', 'http://c.speedtest.net/flash/speedtest.swf') 665 | f = urlopen(req) 666 | response = f.read() 667 | code = f.code 668 | f.close() 669 | 670 | if int(code) != 200: 671 | print_('Could not submit results to speedtest.net') 672 | sys.exit(1) 673 | 674 | qsargs = parse_qs(response.decode()) 675 | resultid = qsargs.get('resultid') 676 | if not resultid or len(resultid) != 1: 677 | print_('Could not submit results to speedtest.net') 678 | sys.exit(1) 679 | 680 | print_('Share results: http://www.speedtest.net/result/%s.png' % 681 | resultid[0]) 682 | 683 | 684 | def main(): 685 | try: 686 | speedtest() 687 | except KeyboardInterrupt: 688 | print_('\nCancelling...') 689 | 690 | 691 | if __name__ == '__main__': 692 | main() 693 | 694 | # vim:ts=4:sw=4:expandtab 695 | -------------------------------------------------------------------------------- /css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.4.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.4.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.4.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.4.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.4.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.4.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.4.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"} 5 | -------------------------------------------------------------------------------- /js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------