Initial commit of WIP

This commit is contained in:
liaralabs 2020-03-07 14:34:57 -08:00
commit b5a598b733
62 changed files with 1487 additions and 0 deletions

10
core/config.py Normal file
View 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
View file

2
core/custom/profiles.py Normal file
View file

@ -0,0 +1,2 @@
from core.profiles import *

213
core/profiles.py Normal file
View 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
View 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
View file

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
flask
flask-htpasswd
flask-socketio
psutil
eventlet
requests

153
static/css/swizzin.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/img/apps/btsync.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/img/apps/csf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
static/img/apps/deluge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/apps/emby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
static/img/apps/flood.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/img/apps/jackett.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/img/apps/lidarr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
static/img/apps/netdata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/img/apps/nzbget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
static/img/apps/plex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
static/img/apps/pyload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/img/apps/radarr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/img/apps/sabnzbd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
static/img/apps/sonarr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/img/apps/znc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

90
static/js/swizzin.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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>

4
wsgi.py Normal file
View file

@ -0,0 +1,4 @@
from swizzin import app
if __name__ == "__main__":
app.run()