Initial commit of WIP
10
core/config.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
import pwd
|
||||
|
||||
class Config:
|
||||
ADMIN_USER = pwd.getpwuid(1000).pw_name
|
||||
APPLICATION_ROOT = '/'
|
||||
FLASK_HTPASSWD_PATH = '/etc/htpasswd'
|
||||
FLASK_SECRET = "What's the password?"
|
||||
HOST = "0.0.0.0"
|
||||
PORT = "8333"
|
||||
URL_BASE = "/"
|
0
core/custom/.gitignore
vendored
Normal file
2
core/custom/profiles.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from core.profiles import *
|
||||
|
213
core/profiles.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
class autodl_meta:
|
||||
name = "irssi"
|
||||
pretty_name = "AutoDL irssi"
|
||||
systemd = "irssi@"
|
||||
multiuser = True
|
||||
|
||||
class bazarr_meta:
|
||||
name = "bazarr"
|
||||
pretty_name = "Bazarr"
|
||||
baseurl = "/bazarr"
|
||||
|
||||
class btsync_meta:
|
||||
name = "btsync"
|
||||
pretty_name = "Resilio Sync"
|
||||
baseurl = ":8888/web"
|
||||
scheme = "http"
|
||||
systemd = "resilio-sync"
|
||||
process = "rslsync"
|
||||
|
||||
class couchpotato_meta:
|
||||
name= "couchpotato"
|
||||
pretty_name = "CouchPotato"
|
||||
baseurl = "/couchpotato"
|
||||
systemd = "couchpotato@"
|
||||
|
||||
class deluge_meta:
|
||||
name = "deluge"
|
||||
pretty_name = "Deluge"
|
||||
baseurl = "/deluge"
|
||||
systemd = "deluged@"
|
||||
multiuser = True
|
||||
|
||||
class delugeweb_meta:
|
||||
name = "delugeweb"
|
||||
pretty_name = "Deluge Web"
|
||||
systemd = "deluge-web@"
|
||||
multiuser = True
|
||||
|
||||
|
||||
class emby_meta:
|
||||
name = "emby"
|
||||
pretty_name = "Emby"
|
||||
baseurl = "/emby"
|
||||
runas = "emby"
|
||||
systemd = "emby-server"
|
||||
|
||||
class filebrowser_meta:
|
||||
name = "filebrowser"
|
||||
pretty_name = "Filebrowser"
|
||||
baseurl = "/filebrowser"
|
||||
|
||||
class flood_meta:
|
||||
name = "flood"
|
||||
pretty_name = "Flood"
|
||||
baseurl = "/flood"
|
||||
systemd = "flood@"
|
||||
multiuser = True
|
||||
|
||||
class headphones_meta:
|
||||
name = "headphones"
|
||||
pretty_name = "Headphones"
|
||||
baseurl = "/headphones"
|
||||
|
||||
class jackett_meta:
|
||||
name = "jackett"
|
||||
pretty_name = "Jackett"
|
||||
baseurl = "/jackett"
|
||||
systemd = "jackett@"
|
||||
|
||||
class lidarr_meta:
|
||||
name = "lidarr"
|
||||
pretty_name = "Lidarr"
|
||||
baseurl = "/lidarr"
|
||||
|
||||
class lounge_meta:
|
||||
name = "lounge"
|
||||
pretty_name = "The Lounge"
|
||||
baseurl = "/lounge"
|
||||
runas = "lounge"
|
||||
|
||||
class medusa_meta:
|
||||
name = "medusa"
|
||||
pretty_name = "Medusa"
|
||||
baseurl = "/medusa"
|
||||
systemd = "medusa@"
|
||||
|
||||
class netdata_meta:
|
||||
name = "netdata"
|
||||
pretty_name = "Netdata"
|
||||
baseurl = "/netdata"
|
||||
runas = "netdata"
|
||||
|
||||
class nextcloud_meta:
|
||||
name = "nextcloud"
|
||||
pretty_name = "Nextcloud"
|
||||
baseurl = "/nextcloud"
|
||||
systemd = False
|
||||
|
||||
class nzbget_meta:
|
||||
name = "nzbget"
|
||||
pretty_name = "nzbGet"
|
||||
baseurl = "/nzbget"
|
||||
systemd = "nzbget@"
|
||||
|
||||
class nzbhydra_meta:
|
||||
name = "nzbhydra"
|
||||
pretty_name = "nzbhydra"
|
||||
baseurl = "/nzbhydra"
|
||||
systemd = "nzbhydra@"
|
||||
|
||||
class ombi_meta:
|
||||
name = "ombi"
|
||||
pretty_name = "Ombi"
|
||||
baseurl = "/ombi"
|
||||
runas = "ombi"
|
||||
|
||||
class plex_meta:
|
||||
name = "plex"
|
||||
pretty_name = "Plex"
|
||||
baseurl = ":32400/web"
|
||||
runas = "plex"
|
||||
process = "Plex"
|
||||
systemd = "plexmediaserver"
|
||||
|
||||
class pyload_meta:
|
||||
name = "pyload"
|
||||
pretty_name = "pyLoad"
|
||||
baseurl = "/pyload"
|
||||
systemd = "pyload@"
|
||||
|
||||
class quassel_meta:
|
||||
name = "quassel"
|
||||
pretty_name = "Quassel-Core"
|
||||
systemd = "quasselcore"
|
||||
|
||||
class radarr_meta:
|
||||
name = "radarr"
|
||||
pretty_name = "Radarr"
|
||||
baseurl = "/radarr"
|
||||
|
||||
class rapidleech_meta:
|
||||
name = "rapidleech"
|
||||
pretty_name = "RapidLeech"
|
||||
baseurl = "/rapidleech"
|
||||
|
||||
class rtorrent_meta:
|
||||
name = "rtorrent"
|
||||
pretty_name = "rTorrent"
|
||||
systemd = "rtorrent@"
|
||||
multiuser = True
|
||||
|
||||
class rutorrent_meta:
|
||||
name = "rutorrent"
|
||||
pretty_name = "ruTorrent"
|
||||
baseurl = "/rutorrent"
|
||||
systemd = False
|
||||
multiuser = True
|
||||
|
||||
class sabnzbd_meta:
|
||||
name = "sabnzbd"
|
||||
pretty_name = "SABnzbd"
|
||||
baseurl = "/sabnzbd"
|
||||
systemd = "sabnzbd@"
|
||||
|
||||
class shellinabox_meta:
|
||||
name = "shellinabox"
|
||||
pretty_name = "Console"
|
||||
baseurl = "/shell"
|
||||
runas = "shellinabox"
|
||||
|
||||
class sickchill_meta:
|
||||
name = "sickchill"
|
||||
pretty_name = "SickChill"
|
||||
baseurl = "/sickchill"
|
||||
systemd = "sickchill@"
|
||||
|
||||
class sickgear_meta:
|
||||
name = "sickgear"
|
||||
pretty_name = "SickGear"
|
||||
baseurl = "/sickgear"
|
||||
systemd = "sickgear@"
|
||||
|
||||
class sonarr_meta:
|
||||
name = "sonarr"
|
||||
pretty_name = "Sonarr"
|
||||
baseurl = "/sonarr"
|
||||
systemd = "sonarr@"
|
||||
|
||||
class subsonic_meta:
|
||||
name = "subsonic"
|
||||
pretty_name = "Subsonic"
|
||||
baseurl = "/subsonic"
|
||||
|
||||
class syncthing_meta:
|
||||
name = "syncthing"
|
||||
pretty_name = "Syncthing"
|
||||
baseurl = "/syncthing"
|
||||
systemd = "syncthing@"
|
||||
|
||||
class tautulli_meta:
|
||||
name = "tautulli"
|
||||
pretty_name = "Tautulli"
|
||||
baseurl = "/tautulli"
|
||||
runas = "tautulli"
|
||||
|
||||
class xmrig_meta:
|
||||
name = "xmrig"
|
||||
pretty_name = "XMRig"
|
||||
|
||||
class znc_meta:
|
||||
name = "znc"
|
||||
pretty_name = "ZNC"
|
||||
runas = "znc"
|
216
core/util.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
import sys
|
||||
import os
|
||||
from core.profiles import *
|
||||
from flask import request, current_app
|
||||
from flask_socketio import SocketIO, emit
|
||||
import subprocess as sp
|
||||
import json
|
||||
import shutil
|
||||
import datetime
|
||||
from pwd import getpwnam
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from core.custom.profiles import *
|
||||
except:
|
||||
pass
|
||||
|
||||
boottimestamp = os.stat("/proc").st_ctime
|
||||
boottimeutc = datetime.datetime.fromtimestamp(boottimestamp).strftime('%b %d, %Y %H:%M:%S')
|
||||
|
||||
def str_to_class(str):
|
||||
return getattr(sys.modules[__name__], str)
|
||||
|
||||
def get_default_interface():
|
||||
"""Get the default interface directly from /proc."""
|
||||
with open("/proc/net/route") as route:
|
||||
for line in route:
|
||||
fields = line.strip().split()
|
||||
if fields[1] != '00000000' or not int(fields[3], 16) & 2:
|
||||
continue
|
||||
return fields[0]
|
||||
|
||||
def generate_page_list(user):
|
||||
admin_user = current_app.config['ADMIN_USER']
|
||||
pages = []
|
||||
locks = os.listdir('/install')
|
||||
try:
|
||||
host = request.host.split(":")[0]
|
||||
except:
|
||||
host = request.host
|
||||
|
||||
scheme = request.scheme
|
||||
for lock in locks:
|
||||
app = lock.split(".")[1]
|
||||
try:
|
||||
profile = str_to_class(app+"_meta")
|
||||
except:
|
||||
continue
|
||||
try:
|
||||
multiuser = profile.multiuser
|
||||
except:
|
||||
multiuser = False
|
||||
if multiuser == False and user != admin_user:
|
||||
continue
|
||||
try:
|
||||
scheme = profile.scheme
|
||||
except:
|
||||
scheme = request.scheme
|
||||
try:
|
||||
url = scheme+"://"+host+profile.baseurl
|
||||
except:
|
||||
url = False
|
||||
try:
|
||||
systemd = profile.systemd
|
||||
except:
|
||||
systemd = profile.name
|
||||
pages.append({"name": profile.name, "pretty_name": profile.pretty_name, "url": url, "systemd": systemd})
|
||||
return pages
|
||||
|
||||
def apps_status(username):
|
||||
apps = []
|
||||
admin_user = current_app.config['ADMIN_USER']
|
||||
locks = os.listdir('/install')
|
||||
ps = sp.Popen(('ps', 'axo', 'user:20,comm,cmd'), stdout=sp.PIPE).communicate()[0]
|
||||
procs = ps.splitlines()
|
||||
for lock in locks:
|
||||
application = lock.split(".")[1]
|
||||
try:
|
||||
profile = str_to_class(application+"_meta")
|
||||
except:
|
||||
continue
|
||||
try:
|
||||
multiuser = profile.multiuser
|
||||
except:
|
||||
multiuser = False
|
||||
if multiuser == False and username != admin_user:
|
||||
continue
|
||||
try:
|
||||
#If application is not run as user
|
||||
user = profile.runas
|
||||
except:
|
||||
user = username
|
||||
try:
|
||||
#If application in `ps` has another name
|
||||
application = profile.process
|
||||
except:
|
||||
application = profile.name
|
||||
try:
|
||||
systemd = profile.systemd
|
||||
except:
|
||||
systemd = profile.name
|
||||
if systemd == False:
|
||||
continue
|
||||
try:
|
||||
enabled = is_application_enabled(systemd, user)
|
||||
except:
|
||||
enabled = False
|
||||
|
||||
status = is_process_running(procs, user, application)
|
||||
apps.append({"name": profile.name, "active": status, "enabled": enabled})
|
||||
return apps
|
||||
|
||||
def is_process_running(procs, username, application):
|
||||
result = False
|
||||
for p in procs:
|
||||
if username.lower() in str(p).lower():
|
||||
if application.lower() in str(p).lower():
|
||||
#print("True")
|
||||
result = True
|
||||
#print(result)
|
||||
return result
|
||||
|
||||
def is_application_enabled(application, user):
|
||||
if "@" in application:
|
||||
result = os.path.exists('/etc/systemd/system/multi-user.target.wants/{application}{user}.service'.format(application=application, user=user))
|
||||
#result = sp.run(('systemctl', 'is-enabled', application+user), stdout=sp.DEVNULL).returncode
|
||||
else:
|
||||
result = os.path.exists('/etc/systemd/system/multi-user.target.wants/{application}.service'.format(application=application))
|
||||
#result = sp.run(('systemctl', 'is-enabled', application), stdout=sp.DEVNULL).returncode
|
||||
#if result == 0:
|
||||
# result = True
|
||||
#else:
|
||||
# result = False
|
||||
return result
|
||||
|
||||
def systemctl(function, application):
|
||||
if function in ("enable", "disable"):
|
||||
result = sp.run(('sudo', 'systemctl', function, '--now', application), stdout=sp.DEVNULL).returncode
|
||||
else:
|
||||
result = sp.run(('sudo', 'systemctl', function, application), stdout=sp.DEVNULL).returncode
|
||||
return result
|
||||
|
||||
def vnstat_data(interface, mode):
|
||||
vnstat = sp.run(('vnstat', '-i', interface, '--json', mode), stdout=sp.PIPE)
|
||||
data = json.loads(vnstat.stdout.decode('utf-8'))
|
||||
#data = vnstat.stdout.decode('utf-8')
|
||||
return data
|
||||
|
||||
def vnstat_parse(interface, mode, query, position):
|
||||
result = vnstat_data(interface, mode)['interfaces'][0]['traffic'][query][position]
|
||||
result['rx'] = GetHumanReadableKB(result['rx'])
|
||||
result['tx'] = GetHumanReadableKB(result['tx'])
|
||||
return result
|
||||
|
||||
def disk_usage(location):
|
||||
total, used, free = shutil.disk_usage(location)
|
||||
totalh = GetHumanReadableB(total)
|
||||
usedh = GetHumanReadableB(used)
|
||||
freeh = GetHumanReadableB(free)
|
||||
usage = '{0:.2f}'.format((used / total * 100))
|
||||
return totalh, usedh, freeh, usage
|
||||
|
||||
def quota_usage(username):
|
||||
quota = sp.Popen(('quota', '-wpu', username), stdout=sp.PIPE)
|
||||
quota = quota.communicate()[0].decode("utf-8").split('\n')[2].split()
|
||||
fs = quota[0]
|
||||
used = quota[1]
|
||||
total = quota[2]
|
||||
free = total - used
|
||||
totalh = GetHumanReadableKB(total)
|
||||
usedh = GetHumanReadableKB(used)
|
||||
freeh = GetHumanReadableKB(free)
|
||||
usage = '{0:.2f}'.format((used / total * 100))
|
||||
return totalh, usedh, freeh, usage
|
||||
|
||||
def GetHumanReadableKB(size,precision=2):
|
||||
suffixes=['KB','MB','GB','TB','PB']
|
||||
suffixIndex = 0
|
||||
while size > 1024 and suffixIndex < 4:
|
||||
suffixIndex += 1 #increment the index of the suffix
|
||||
size = size/1024.0 #apply the division
|
||||
return "%.*f %s"%(precision,size,suffixes[suffixIndex])
|
||||
|
||||
def GetHumanReadableB(size,precision=2):
|
||||
suffixes=['B','KB','MB','GB','TB','PB']
|
||||
suffixIndex = 0
|
||||
while size > 1024 and suffixIndex < 4:
|
||||
suffixIndex += 1 #increment the index of the suffix
|
||||
size = size/1024.0 #apply the division
|
||||
return "%.*f %s"%(precision,size,suffixes[suffixIndex])
|
||||
|
||||
def get_nic_bytes(t, interface):
|
||||
with open('/sys/class/net/' + interface + '/statistics/' + t + '_bytes', 'r') as f:
|
||||
data = f.read();
|
||||
return int(data)
|
||||
|
||||
def get_uid(user):
|
||||
result = getpwnam(user).pw_uid
|
||||
return result
|
||||
|
||||
|
||||
#https://stackoverflow.com/questions/41431882/live-stream-stdout-and-stdin-with-websocket
|
||||
## panel threading install idea
|
||||
#async def time(websocket, path):
|
||||
# script_name = 'script.py'
|
||||
# script = await websocket.recv()
|
||||
# with open(script_name, 'w') as script_file:
|
||||
# script_file.write(script)
|
||||
# with subprocess.Popen(['python3', '-u', script_name],
|
||||
# stdout=subprocess.PIPE,
|
||||
# bufsize=1,
|
||||
# universal_newlines=True) as process:
|
||||
# for line in process.stdout:
|
||||
# line = line.rstrip()
|
||||
# print(f"line = {line}")
|
||||
# await websocket.send(line)
|
0
gunicorn.sh
Normal file
6
requirements.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
flask
|
||||
flask-htpasswd
|
||||
flask-socketio
|
||||
psutil
|
||||
eventlet
|
||||
requests
|
153
static/css/swizzin.css
Normal file
|
@ -0,0 +1,153 @@
|
|||
a[post=true] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.table-condensed > thead > tr > td,
|
||||
.table-condensed > tbody > tr > td,
|
||||
.table-condensed > tfoot > tr > td {
|
||||
padding: 2px;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.table-condensed > thead > tr > th,
|
||||
.table-condensed > tbody > tr > th,
|
||||
.table-condensed > tfoot > tr > th {
|
||||
padding:3px;
|
||||
font-size:18px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
font-size: 9px;
|
||||
cursor: default;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px
|
||||
}
|
||||
|
||||
.systemindicator {
|
||||
font-size: 9px;
|
||||
cursor: default;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 15rem;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
a.grayscale img {
|
||||
filter: saturate(25%);
|
||||
}
|
||||
|
||||
a.grayscale:hover img {
|
||||
filter: saturate(80%);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
vertical-align: center;
|
||||
width: 28px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.toggle.btn-xs {
|
||||
min-width: 3.7rem;
|
||||
}
|
||||
.toggle-on.btn-xs {
|
||||
padding-right: .4rem;
|
||||
}
|
||||
|
||||
.toggle-off.btn-xs {
|
||||
padding-left: .4rem;
|
||||
}
|
||||
|
||||
/*.btn-group-xs > .btn, .btn-xs {
|
||||
padding: .25rem .4rem;
|
||||
font-size: .875rem;
|
||||
line-height: .5;
|
||||
border-radius: .2rem;
|
||||
}*/
|
||||
|
||||
/*.btn-xs,
|
||||
.btn-group-xs > .btn {
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}*/
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#sidebar-wrapper {
|
||||
min-height: 100vh;
|
||||
margin-left: -15rem;
|
||||
-webkit-transition: margin .25s ease-out;
|
||||
-moz-transition: margin .25s ease-out;
|
||||
-o-transition: margin .25s ease-out;
|
||||
transition: margin .25s ease-out;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar-heading {
|
||||
width: 15rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .list-group {
|
||||
}
|
||||
|
||||
#page-content-wrapper {
|
||||
min-width: 100vw;
|
||||
}
|
||||
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#sidebar-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#page-content-wrapper {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
margin-left: -15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.card-columns {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.card-columns {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.card-columns {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.card-columns {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
BIN
static/img/apps/bazarr.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/img/apps/btsync.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/img/apps/couchpotato.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
static/img/apps/csf.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
static/img/apps/deluge.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/img/apps/emby.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/apps/filebrowser.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
static/img/apps/flood.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
static/img/apps/headphones.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/img/apps/jackett.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
static/img/apps/lidarr.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
7
static/img/apps/lounge.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg enable-background="new 0 0 512 512" height="512px" id="Layer_1" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="chat_x5F_support">
|
||||
<path d="M170.542,357.786c-15.944-2.444-31.341-6.704-46.024-12.779l-5.493-2.272l-44.78,16.973l15.091-33.515 l-8.914-7.281C48.552,292.879,31,258.503,31,222.119C31,145.96,108.141,84,202.96,84c66.491,0,124.284,30.47,152.885,74.937 c6.45,0.715,12.819,1.716,19.08,3.013C346.379,107.298,280.133,69,202.96,69C99.705,69,16,137.554,16,222.119 c0,42.354,20.999,80.691,54.934,108.411l-25.235,56.04l73.085-27.701c18.762,7.762,39.34,13.007,61.07,15.203 C176.34,368.804,173.231,363.368,170.542,357.786z" fill="#818B9E" />
|
||||
<path d="M492,303.273c0-72.144-71.411-130.629-159.5-130.629S173,231.128,173,303.273s71.411,130.629,159.5,130.629 c25.834,0,50.229-5.036,71.813-13.965l62.35,23.633l-21.528-47.809C474.085,372.112,492,339.406,492,303.273z M253.5,334.606 c-14.636,0-26.5-11.864-26.5-26.5s11.864-26.5,26.5-26.5c14.636,0,26.5,11.864,26.5,26.5S268.136,334.606,253.5,334.606z M332.5,334.606c-14.636,0-26.5-11.864-26.5-26.5s11.864-26.5,26.5-26.5s26.5,11.864,26.5,26.5S347.136,334.606,332.5,334.606z M411.5,334.606c-14.636,0-26.5-11.864-26.5-26.5s11.864-26.5,26.5-26.5s26.5,11.864,26.5,26.5S426.136,334.606,411.5,334.606z" fill="#f2f3f5" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/img/apps/medusa.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
static/img/apps/netdata.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/img/apps/nextcloud.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/apps/nzbget.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
static/img/apps/nzbhydra.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
static/img/apps/plex.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
static/img/apps/plexrequests-net.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
static/img/apps/pyload.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
static/img/apps/quickbox.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/img/apps/radarr.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
static/img/apps/rapidleech.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
static/img/apps/rutorrent.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
static/img/apps/sabnzbd.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
static/img/apps/sickchill.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/img/apps/sickgear.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
static/img/apps/sonarr.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/apps/subsonic.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
static/img/apps/syncthing.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
static/img/apps/tautulli.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/img/apps/znc.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
static/img/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
static/img/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
9
static/img/favicon/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#222222</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
static/img/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 775 B |
BIN
static/img/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
static/img/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
18
static/img/favicon/manifest.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "QuickBox",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
static/img/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
24
static/img/favicon/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,16.000000) scale(0.002667,-0.002667)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M174 5976 c-65 -30 -120 -85 -150 -150 l-24 -51 0 -2775 0 -2775 24
|
||||
-51 c30 -65 85 -120 150 -150 l51 -24 2775 0 2775 0 51 24 c65 30 120 85 150
|
||||
150 l24 51 0 2775 0 2775 -24 51 c-30 65 -85 120 -150 150 l-51 24 -2775 0
|
||||
-2775 0 -51 -24z m5335 -591 c59 -32 112 -90 137 -151 25 -59 21 -173 -8 -254
|
||||
-19 -50 -2301 -3974 -2452 -4215 -44 -71 -103 -133 -151 -158 -59 -32 -201
|
||||
-31 -267 1 -99 49 -166 142 -175 241 -7 81 21 158 115 313 44 73 591 1011
|
||||
1217 2083 625 1073 1152 1974 1171 2003 46 71 107 130 157 153 64 30 184 23
|
||||
256 -16z m-4114 -2900 c40 -21 72 -47 97 -80 66 -86 69 -111 66 -482 l-3 -328
|
||||
-27 -51 c-36 -68 -115 -138 -172 -153 -29 -7 -173 -11 -414 -11 -421 0 -432 2
|
||||
-506 76 -90 90 -105 156 -105 489 -1 225 10 348 35 397 47 94 121 152 213 168
|
||||
25 5 206 8 402 7 l355 -2 59 -30z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/img/logo-dark.png
Normal file
After Width: | Height: | Size: 11 KiB |
90
static/js/swizzin.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
window.onload = function() {
|
||||
$.get('/stats/boot', function(data) {
|
||||
countUpFromTime(data, 'uptime');
|
||||
})
|
||||
};
|
||||
function countUpFromTime(countFrom, id) {
|
||||
countFrom = new Date(countFrom).getTime();
|
||||
var now = new Date(),
|
||||
countFrom = new Date(countFrom),
|
||||
timeDifference = (now - countFrom);
|
||||
|
||||
var secondsInADay = 60 * 60 * 1000 * 24,
|
||||
secondsInAHour = 60 * 60 * 1000;
|
||||
|
||||
days = Math.floor(timeDifference / (secondsInADay) * 1);
|
||||
hours = Math.floor((timeDifference % (secondsInADay)) / (secondsInAHour) * 1);
|
||||
mins = Math.floor(((timeDifference % (secondsInADay)) % (secondsInAHour)) / (60 * 1000) * 1);
|
||||
secs = Math.floor((((timeDifference % (secondsInADay)) % (secondsInAHour)) % (60 * 1000)) / 1000 * 1);
|
||||
|
||||
var idEl = document.getElementById(id);
|
||||
idEl.getElementsByClassName('days')[0].innerHTML = days;
|
||||
idEl.getElementsByClassName('hours')[0].innerHTML = hours;
|
||||
idEl.getElementsByClassName('minutes')[0].innerHTML = mins;
|
||||
idEl.getElementsByClassName('seconds')[0].innerHTML = secs;
|
||||
|
||||
clearTimeout(countUpFromTime.interval);
|
||||
countUpFromTime.interval = setTimeout(function(){ countUpFromTime(countFrom, id); }, 1000);
|
||||
}
|
||||
|
||||
$("#menu-toggle").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#wrapper").toggleClass("toggled");
|
||||
});
|
||||
|
||||
function makePostRequest(url, data, callback) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: data,
|
||||
contentType: "application/json",
|
||||
success: function (result) {
|
||||
if(typeof callback == 'function') {
|
||||
clearTimeout(timer);
|
||||
callback.call();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function(){
|
||||
$("a[post=true]").each(function () {
|
||||
$(this).on('click', function () {
|
||||
makePostRequest(
|
||||
$(this).attr('phref'),
|
||||
$(this).attr('pdata'),
|
||||
appstatus
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(function(){
|
||||
$("input[post=true]").each(function () {
|
||||
$(this).on('change', function () {
|
||||
var data = JSON.parse($(this).attr('pdata'));
|
||||
data.function = $(this).is(':checked') ? 'enable' : 'disable';
|
||||
data = JSON.stringify(data)
|
||||
makePostRequest(
|
||||
$(this).attr('phref'),
|
||||
data,
|
||||
appstatus
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
var protocol = window.location.protocol;
|
||||
var socket = io.connect(protocol + '//' + document.domain + ':' + location.port + '/websocket');
|
||||
socket.on('speed', function(result) {
|
||||
$('#current_rx').html(result.rx);
|
||||
$('#current_tx').html(result.tx)
|
||||
$('#current_interface').html(result.interface)
|
||||
return false;
|
||||
});
|
||||
socket.on('iowait', function(result) {
|
||||
$('#iowait-glance').html(result.iowait);
|
||||
return false;
|
||||
});
|
||||
});
|
220
swizzin.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
#!/root/flask/bin/python
|
||||
import flask
|
||||
from flask_htpasswd import HtPasswdAuth
|
||||
from flask_socketio import SocketIO, emit
|
||||
from threading import Thread, Lock
|
||||
import os
|
||||
from core.util import *
|
||||
import core.config
|
||||
import requests
|
||||
import time
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import calendar
|
||||
import psutil
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
async_mode = None
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
socketio = SocketIO(app, async_mode=async_mode)
|
||||
|
||||
app.config.from_object('core.config.Config')
|
||||
app.config.from_pyfile('swizzin.cfg', silent=True)
|
||||
|
||||
#app.config['FLASK_HTPASSWD_PATH'] = '/etc/htpasswd'
|
||||
#app.config['FLASK_SECRET'] = "What's the password?"
|
||||
admin_user = app.config['ADMIN_USER']
|
||||
|
||||
htpasswd = HtPasswdAuth(app)
|
||||
thread = None
|
||||
thread_lock = Lock()
|
||||
thread2 = None
|
||||
thread2_lock = Lock()
|
||||
|
||||
def current_speed(app):
|
||||
""" Thread for interface speed """
|
||||
with app.app_context():
|
||||
#print("Starting current speed for", interface)
|
||||
interface = get_default_interface()
|
||||
(tx_prev, rx_prev) = (0, 0)
|
||||
while(True):
|
||||
tx = get_nic_bytes('tx', interface)
|
||||
rx = get_nic_bytes('rx', interface)
|
||||
if tx_prev > 0:
|
||||
tx_speed = tx - tx_prev
|
||||
#print('TX: ', tx_speed, 'bps')
|
||||
tx_speed = str(GetHumanReadableB(tx_speed)) + "/s"
|
||||
if rx_prev > 0:
|
||||
rx_speed = rx - rx_prev
|
||||
#print('RX: ', rx_speed, 'bps')
|
||||
rx_speed = str(GetHumanReadableB(rx_speed)) + "/s"
|
||||
emit('speed', {'interface': interface, 'tx': tx_speed, 'rx': rx_speed}, namespace='/websocket', broadcast=True)
|
||||
time.sleep(1)
|
||||
tx_prev = tx
|
||||
rx_prev = rx
|
||||
|
||||
def io_wait(app):
|
||||
""" Thread for iowait emission """
|
||||
with app.app_context():
|
||||
while(True):
|
||||
times = psutil.cpu_times_percent(interval=10)
|
||||
#print(times.iowait)
|
||||
emit('iowait', {'iowait': times.iowait}, namespace='/websocket', broadcast=True)
|
||||
|
||||
@app.route('/')
|
||||
@htpasswd.required
|
||||
def index(user):
|
||||
#global thread
|
||||
#if thread is None:
|
||||
# thread = Thread(target=current_speed)
|
||||
# thread.start()
|
||||
pages = generate_page_list(user)
|
||||
return flask.render_template('index.html', title='{user} - swizzin dashboard'.format(user=user), user=user, pages=pages, async_mode=socketio.async_mode)
|
||||
|
||||
@socketio.on('connect', namespace='/websocket')
|
||||
def socket_connect():
|
||||
global thread
|
||||
global thread2
|
||||
with thread_lock:
|
||||
if thread is None:
|
||||
thread = socketio.start_background_task(current_speed, (flask.current_app._get_current_object()))
|
||||
with thread2_lock:
|
||||
if thread2 is None:
|
||||
thread2 = socketio.start_background_task(io_wait, (flask.current_app._get_current_object()))
|
||||
emit('my_response', {'data': 'Connected', 'count': 0})
|
||||
|
||||
@app.route('/stats')
|
||||
@app.route('/stats/')
|
||||
@htpasswd.required
|
||||
def stats(user):
|
||||
pages = generate_page_list(user)
|
||||
return flask.render_template('stats.html', title='Stats', user=user, pages=pages)
|
||||
|
||||
@app.route('/stats/netdata/')
|
||||
@app.route('/stats/netdata/<path:p>',methods=['GET','POST',"DELETE"])
|
||||
@htpasswd.required
|
||||
def netdataproxy(user, p = ''):
|
||||
SITE = 'http://127.0.0.1:19999/{}'.format(p)
|
||||
if flask.request.method=='GET':
|
||||
if flask.request.args:
|
||||
querystring = flask.request.query_string.decode('utf-8')
|
||||
resp = requests.get(f'{SITE}?{querystring}')
|
||||
else:
|
||||
resp = requests.get(f'{SITE}')
|
||||
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
||||
headers = [(name, value) for (name, value) in resp.raw.headers.items() if name.lower() not in excluded_headers]
|
||||
response = flask.Response(resp.content, resp.status_code, headers)
|
||||
return response
|
||||
elif flask.request.method=='POST':
|
||||
resp = requests.post(f'{SITE}',json=request.get_json())
|
||||
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
||||
headers = [(name, value) for (name, value) in resp.raw.headers.items() if name.lower() not in excluded_headers]
|
||||
response = flask.Response(resp.content, resp.status_code, headers)
|
||||
return response
|
||||
elif flask.request.method=='DELETE':
|
||||
resp = requests.delete(f'{SITE}').content
|
||||
response = flask.Response(resp.content, resp.status_code, headers)
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/apps/status')
|
||||
@htpasswd.required
|
||||
def app_status(user):
|
||||
apps = apps_status(user)
|
||||
return flask.jsonify(apps)
|
||||
|
||||
@app.route('/apps/service', methods=['POST'])
|
||||
@htpasswd.required
|
||||
def service(user):
|
||||
if flask.request.method == 'POST':
|
||||
data = flask.request.get_json()
|
||||
application = data['application']
|
||||
try:
|
||||
profile = str_to_class(application+"_meta")
|
||||
except:
|
||||
return """Application profile not found."""
|
||||
try:
|
||||
multiuser = profile.multiuser
|
||||
except:
|
||||
multiuser = False
|
||||
if multiuser == False and user != admin_user:
|
||||
return """Access denied"""
|
||||
try:
|
||||
application = profile.systemd
|
||||
except:
|
||||
pass
|
||||
|
||||
if "@" in application:
|
||||
application = application+user
|
||||
result = systemctl(data['function'], application)
|
||||
return str(result)
|
||||
|
||||
|
||||
@app.route('/stats/loadavg')
|
||||
@htpasswd.required
|
||||
def loadavg(user):
|
||||
loadavg = open("/proc/loadavg").readline().split(" ")[:3]
|
||||
numcpu = os.cpu_count()
|
||||
perutil = '{0:.2f}'.format((float(loadavg[0]) / numcpu) * 100)
|
||||
return flask.jsonify({"1m": loadavg[0], "5m": loadavg[1], "15m": loadavg[2], "perutil": perutil})
|
||||
|
||||
@app.route('/stats/vnstat')
|
||||
@htpasswd.required
|
||||
def vnstat(user):
|
||||
stats = []
|
||||
interface = "eno1"
|
||||
statsh = vnstat_parse(interface, "h", "hours", 0)
|
||||
statslh = vnstat_parse(interface, "h", "hours", 1)
|
||||
statsd = vnstat_parse(interface, "d", "days", 0)
|
||||
statsm = vnstat_parse(interface, "m", "months", 0)
|
||||
statsa = ''
|
||||
#statsa = vnstat_parse(interface, "m", "total", 0)
|
||||
tops = vnstat_data(interface, "t")['interfaces'][0]['traffic']['tops']
|
||||
top = []
|
||||
for t in tops:
|
||||
date = t['date']
|
||||
year = date['year']
|
||||
month = calendar.month_abbr[date['month']]
|
||||
day = date['day']
|
||||
date = "{month} {day}, {year}".format(year=year, month=month, day=day)
|
||||
rx = GetHumanReadableKB(t['rx'])
|
||||
tx = GetHumanReadableKB(t['tx'])
|
||||
top.append({"date": date, "rx": rx, "tx": tx})
|
||||
columns = {"date", "rx", "tx"}
|
||||
#stats = []
|
||||
#stats.extend([statsh, statslh, statsd, statsm, top])
|
||||
#print(stats)
|
||||
return flask.render_template('top.html', top=top, day=statsd, month=statsm, hour=statsh, lasthour=statslh, alltime=statsa, colnames=columns)
|
||||
|
||||
@app.route('/stats/disk')
|
||||
@htpasswd.required
|
||||
def disk_free(user):
|
||||
location = "/"
|
||||
if os.path.isfile("/install/.quota.lock"):
|
||||
total, used, free, usage = quota_usage(user)
|
||||
else:
|
||||
total, used, free, usage = disk_usage(location)
|
||||
return flask.jsonify({"disktotal": total, "diskused": used, "diskfree": free, "perutil": usage})
|
||||
|
||||
@app.route('/stats/boot')
|
||||
@htpasswd.required
|
||||
def boot_time(user):
|
||||
return boottimeutc
|
||||
|
||||
@app.route('/stats/ram')
|
||||
@htpasswd.required
|
||||
def ram_stats(user):
|
||||
ramstats = dict((i.split()[0].rstrip(':'),int(i.split()[1])) for i in open('/proc/meminfo').readlines())
|
||||
ramtotal = GetHumanReadableKB(ramstats['MemTotal'])
|
||||
ramfree = GetHumanReadableKB(ramstats['MemAvailable'])
|
||||
ramused = GetHumanReadableKB(ramstats['MemTotal'] - ramstats['MemAvailable'])
|
||||
perutil = '{0:.2f}'.format((ramstats['MemTotal'] - ramstats['MemAvailable']) / ramstats['MemTotal'] * 100)
|
||||
return flask.jsonify({"ramtotal": ramtotal, "ramfree": ramfree, "ramused": ramused, "perutil": perutil})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, host=app.config['HOST'], port=app.config['PORT'])
|
||||
|
||||
#app.run(debug=True,host='0.0.0.0', port=8333)
|
23
templates/diskinfo.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="card border-dark mb-3 mt-3">
|
||||
<div class="card-header">Disk Info</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="text-center">Used</h5>
|
||||
<p class="text-center"><span id="diskused"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">Free</h5>
|
||||
<p class="text-center"><span id="diskfree"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">Total</h5>
|
||||
<p class="text-center"><span id="disktotal"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div id="diskprogress" class="progress-bar bg-warning" role="progressbar" style="" aria-valuenow="" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<p class="text-center">You have used <span id="diskpercent"></span>% of your disk</p>
|
||||
</div>
|
||||
</div>
|
36
templates/glance.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">At a Glance</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="text-center">Load</h5>
|
||||
<p class="text-center"><span id="loadindicator" class="systemindicator"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">Disk</h5>
|
||||
<p class="text-center"><span id="diskindicator" class="systemindicator"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">RAM</h5>
|
||||
<p class="text-center"><span id="ramindicator" class="systemindicator"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">IOWait</h5>
|
||||
<p class="text-center"><span id="iowait-glance">--</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="text-center">Uptime</h5>
|
||||
<div class="countup text-center" id="uptime">
|
||||
<p>
|
||||
<span class=" days">00</span>
|
||||
<span class=" timeRefDays">days</span>
|
||||
<span class=" hours">00</span>
|
||||
<span class=" timeRefHours">hours</span>
|
||||
<span class=" minutes">00</span>
|
||||
<span class=" timeRefMinutes">minutes</span>
|
||||
<span class=" seconds">00</span>
|
||||
<span class=" timeRefSeconds">seconds</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
163
templates/index.html
Normal file
|
@ -0,0 +1,163 @@
|
|||
{% import "macros.html" as macros %}
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/darkly/bootstrap.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/css/bootstrap4-toggle.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/swizzin.css')}}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex" id="wrapper">
|
||||
{{macros.build_site_navigation(pages=pages, selected="Site Details")}}
|
||||
|
||||
<div id="page-content-wrapper">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<button class="btn btn-primary" id="menu-toggle">«</button>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto mt-2 mt-lg-0">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">Home <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('stats') }}">Stats</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ user }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- #page-content-wrapper -->
|
||||
<div class="container-fluid">
|
||||
<div class="card-columns">
|
||||
{% include 'glance.html' %}
|
||||
{{ macros.build_app_table(apps=pages) }}
|
||||
{% include 'systeminfo.html' %}
|
||||
{% include 'diskinfo.html' %}
|
||||
{% include 'raminfo.html' %}
|
||||
<div id="top10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /#page-content-wrapper -->
|
||||
</div>
|
||||
<!-- /#body-content-wrapper -->
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/js/bootstrap4-toggle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/swizzin.js') }}"></script>
|
||||
|
||||
|
||||
<script>
|
||||
function appstatus(){
|
||||
$.get("{{ url_for('app_status') }}", function(data){
|
||||
for (var apps in data) {
|
||||
var name = data[apps]["name"];
|
||||
var enabled = data[apps]["enabled"];
|
||||
var active = data[apps]["active"];
|
||||
if (active == true) {
|
||||
$("#status_"+name+".indicator").addClass("bg-success").removeClass("bg-danger");
|
||||
} else {
|
||||
$("#status_"+name+".indicator").addClass("bg-danger").removeClass("bg-success");
|
||||
}
|
||||
if (enabled == true) {
|
||||
$('#enabled_'+name).data('bs.toggle').on(true);
|
||||
} else {
|
||||
$('#enabled_'+name).data('bs.toggle').off(true);
|
||||
}
|
||||
}
|
||||
timer = setTimeout(function(){appstatus()}, 60000);
|
||||
}
|
||||
)
|
||||
};
|
||||
appstatus();
|
||||
|
||||
(function loadavg() {
|
||||
$.get('{{ url_for('loadavg') }}', function(data) {
|
||||
$("#load1m").html(data['1m']);
|
||||
$("#load5m").html(data['5m']);
|
||||
$("#load15m").html(data['15m']);
|
||||
$("#loadpercent").html(data['perutil']);
|
||||
if (Number(data['perutil']) > 90) {
|
||||
$("#loadindicator.systemindicator").addClass("bg-danger").removeClass("bg-success bg-warning");
|
||||
} else if (Number(data['perutil']) > 75) {
|
||||
$("#loadindicator.systemindicator").addClass("bg-warning").removeClass("bg-success bg-danger");
|
||||
} else {
|
||||
$("#loadindicator.systemindicator").addClass("bg-success").removeClass("bg-warning bg-danger");
|
||||
}
|
||||
setTimeout(function(){loadavg()}, 60000);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
(function diskusage() {
|
||||
$.get('{{ url_for('disk_free') }}', function(data) {
|
||||
var percent = Math.trunc(data['perutil']);
|
||||
$("#diskfree").html(data['diskfree']);
|
||||
$("#diskused").html(data['diskused']);
|
||||
$("#disktotal").html(data['disktotal']);
|
||||
$("#diskpercent").html(data['perutil']);
|
||||
if (Number(data['perutil']) > 90) {
|
||||
$("#diskindicator.systemindicator").addClass("bg-danger").removeClass("bg-success bg-warning");
|
||||
$("#diskprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-danger").removeClass("bg-success bg-warning");
|
||||
} else if (Number(data['perutil']) > 75) {
|
||||
$("#diskindicator.systemindicator").addClass("bg-warning").removeClass("bg-success bg-danger");
|
||||
$("#diskprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-warning").removeClass("bg-success bg-danger");
|
||||
} else {
|
||||
$("#diskindicator.systemindicator").addClass("bg-success").removeClass("bg-warning bg-danger");
|
||||
$("#diskprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-success").removeClass("bg-danger bg-warning");
|
||||
}
|
||||
setTimeout(function(){diskusage()}, 60000);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
(function ramusage() {
|
||||
$.get('{{ url_for('ram_stats') }}', function(data) {
|
||||
var percent = Math.trunc(data['perutil']);
|
||||
$("#ramfree").html(data['ramfree']);
|
||||
$("#ramused").html(data['ramused']);
|
||||
$("#ramtotal").html(data['ramtotal']);
|
||||
$("#rampercent").html(data['perutil']);
|
||||
if (Number(data['perutil']) > 90) {
|
||||
$("#ramindicator.systemindicator").addClass("bg-danger").removeClass("bg-success bg-warning");
|
||||
$("#ramprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-danger").removeClass("bg-success bg-warning");
|
||||
} else if (Number(data['perutil']) > 75) {
|
||||
$("#ramindicator.systemindicator").addClass("bg-warning").removeClass("bg-success bg-danger");
|
||||
$("#ramprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-warning").removeClass("bg-success bg-danger");
|
||||
} else {
|
||||
$("#ramindicator.systemindicator").addClass("bg-success").removeClass("bg-warning bg-danger");
|
||||
$("#ramprogress").css("width", percent + "%").attr("aria-valuenow", percent).addClass("bg-success").removeClass("bg-danger bg-warning");
|
||||
}
|
||||
setTimeout(function(){ramusage()}, 60000);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
(function vnstat_top10() {
|
||||
$.get('{{ url_for('vnstat') }}', function(data) {
|
||||
$("#top10").html(data);
|
||||
setTimeout(function(){vnstat_top10()}, 600000);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
48
templates/macros.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% macro build_site_navigation(pages, selected) %}
|
||||
<div class="bg-dark" id="sidebar-wrapper">
|
||||
<div class="sidebar-heading"><img class="logo" src="{{ url_for('static', filename='img/logo-dark.png') }}" /></div>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for page in pages|sort(attribute="pretty_name") if page.url %}
|
||||
<a href="{{page.url}}" target="_blank" class="list-group-item list-group-item-action bg-dark"><img src="{{ url_for('static', filename='img/apps/'+page.name+'.png') }}" class="app-icon rounded-circle">{{page.pretty_name}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro build_app_table(apps) %}
|
||||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">Service Info</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless table-hover table-sm table-dark">
|
||||
<tbody>
|
||||
<colgroup>
|
||||
<col style="width: 32%;">
|
||||
<col style="width: 30%;">
|
||||
<col style="width: 8%;">
|
||||
<col style="width: 30%;">
|
||||
</colgroup>
|
||||
{% for app in apps|sort(attribute="pretty_name") if app.systemd %}
|
||||
<tr>
|
||||
<td class="align-middle"><span class="align-middle">{{ app.pretty_name }}</span></td>
|
||||
<td class="align-middle text-center"><a post="true" phref="{{ url_for('service') }}" pdata='{"user": "{{ user }}", "function": "restart", "application": "{{ app.name }}"}' class="btn btn-xs btn-secondary"><i class="fa fa-refresh"></i>Restart</a></td>
|
||||
<td class="align-middle text-center"><span id="status_{{ app.name }}" class="indicator"></span></td>
|
||||
<td class="align-middle text-center"><input data-toggle="toggle" post="true" phref="{{ url_for('service') }}" pdata='{"application": "{{ app.name }}"}' type="checkbox" data-size="xs" data-on="Enabled" data-off="Disabled" data-onstyle="outline-success" data-offstyle="outline-danger" id="enabled_{{ app.name }}"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro build_app_table_js(apps) %}
|
||||
{% for app in apps %}
|
||||
(function {{ app.name }}_status() {
|
||||
$.get('{{ url_for('status', p=app.name) }}', function(data) {
|
||||
$("#status_{{ app.name }}").html(data);
|
||||
setTimeout(function(){{ '{' }}{{ app.name }}_status()}, 15000);
|
||||
}
|
||||
);
|
||||
})();
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
23
templates/raminfo.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">RAM Info</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="text-center">Used</h5>
|
||||
<p class="text-center"><span id="ramused"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">Free</h5>
|
||||
<p class="text-center"><span id="ramfree"></span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5 class="text-center">Total</h5>
|
||||
<p class="text-center"><span id="ramtotal"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div id="ramprogress" class="progress-bar bg-warning" role="progressbar" style="" aria-valuenow="" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<p class="text-center">RAM utilization is at <span id="rampercent"></span>%</p>
|
||||
</div>
|
||||
</div>
|
134
templates/stats.html
Normal file
|
@ -0,0 +1,134 @@
|
|||
{% import "macros.html" as macros %}
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/darkly/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('netdataproxy') }}dashboard.css"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/swizzin.css')}}">
|
||||
|
||||
<script>
|
||||
var netdataNoBootstrap = true;
|
||||
var netdataTheme = 'slate';
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex" id="wrapper">
|
||||
{{macros.build_site_navigation(pages=pages, selected="Site Details")}}
|
||||
|
||||
<div id="page-content-wrapper">
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<button class="btn btn-primary" id="menu-toggle">«</button>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto mt-2 mt-lg-0">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">Home <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('stats') }}">Stats</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ user }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">System Input/Output</div>
|
||||
<div class="card-body">
|
||||
<div data-netdata="system.io"
|
||||
data-chart-library="dygraph"
|
||||
data-title="Disk IO (in: read, out: write)"
|
||||
data-append-options="absolute,percentage"
|
||||
data-height="250"
|
||||
data-after="-300"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">System Network</div>
|
||||
<div class="card-body">
|
||||
<div data-netdata="system.net"
|
||||
data-title="Network Usage"
|
||||
data-chart-library="dygraph"
|
||||
data-append-options="absolute,percentage"
|
||||
data-height="250"
|
||||
data-after="-300"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">System Load</div>
|
||||
<div class="card-body">
|
||||
<div data-netdata="system.load"
|
||||
data-title="System Load"
|
||||
data-chart-library="dygraph"
|
||||
data-height="250"
|
||||
data-after="-300"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">System CPU</div>
|
||||
<div class="card-body">
|
||||
<div data-netdata="system.cpu"
|
||||
data-title="CPU usage"
|
||||
data-chart-library="dygraph"
|
||||
data-height="250"
|
||||
data-after="-300"
|
||||
data-dygraph-valuerange="[0, 100]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /#page-content-wrapper -->
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="{{ url_for('netdataproxy') }}dashboard.js"></script>
|
||||
|
||||
<script>
|
||||
$("#menu-toggle").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#wrapper").toggleClass("toggled");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
9
templates/systeminfo.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">System Info</div>
|
||||
<div class="card-body">
|
||||
<h5>Load Average</h5>
|
||||
<p>1m: <span id="load1m"></span> 5m: <span id="load5m"></span> 15m: <span id="load15m"></span></p>
|
||||
<h5>Percent Utilized</h5>
|
||||
<p><span id="loadpercent"></span>%</p>
|
||||
</div>
|
||||
</div>
|
79
templates/top.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<div class="card border-dark mt-3">
|
||||
<div class="card-header">Network Info</div>
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<table class="table table-hover table-condensed table-dark table-borderless">
|
||||
<thead class="table-active">
|
||||
<tr>
|
||||
<th scope="col">Interface</th>
|
||||
<th scope="col" class="text-info text-right">RX</th>
|
||||
<th scope="col" class="text-success">TX</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><div id="current_interface">--</div></td>
|
||||
<td class="text-info"><div id="current_rx" class="text-right">--</div></td>
|
||||
<td class="text-success"><div id="current_tx">--</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed table-hover table-dark table-borderless">
|
||||
<thead class="table-active">
|
||||
<tr>
|
||||
<th scope="col">Span</th>
|
||||
<th scope="col" class="text-info text-right">RX</th>
|
||||
<th scope="col" class="text-success">TX</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>This Hour</td>
|
||||
<td class="text-info text-right">{{ hour['rx'] }}</td>
|
||||
<td class="text-success">{{ hour['tx'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Hour</td>
|
||||
<td class="text-info text-right">{{ lasthour['rx'] }}</td>
|
||||
<td class="text-success">{{ lasthour['tx'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>This Day</td>
|
||||
<td class="text-info text-right">{{ day['rx'] }}</td>
|
||||
<td class="text-success">{{ day['tx'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>This Month</td>
|
||||
<td class="text-info text-right">{{ month['rx'] }}</td>
|
||||
<td class="text-success">{{ month['tx'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>All Time</td>
|
||||
<td class="text-info text-right">{{ alltime['rx'] }}</td>
|
||||
<td class="text-success">{{ alltime['tx'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed table-hover table-dark table-borderless">
|
||||
<thead class="table-active">
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col" class="text-info text-right">RX</th>
|
||||
<th scope="col" class="text-success">TX</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in top %}
|
||||
<tr>
|
||||
<td>{{ t['date'] }}</td>
|
||||
<td class="text-info text-right">{{ t['rx'] }}</td>
|
||||
<td class="text-success">{{ t['tx'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|