瀏覽代碼

Updates v1.122

- Fixed bug in File Manager Share function
- Fixed filename with hash issue
- Fixed Chinese filename decode issue
- Fixed root escape bug using rename function
- Fixed user list API bug
- Fixed WebApp reload permission denied UX bug
- Added auto web.tar.gz auto unzip feature for those who dont't know how to properly unzip .tar.gz file on Windows
- Added Network Usage diagram for Windows and Linux
- Added owner can always access their shared file logic
Toby Chui 3 年之前
父節點
當前提交
4c6d97672c

+ 5 - 5
src/file_system.go

@@ -1003,8 +1003,8 @@ func system_fs_restoreFile(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//OK to proceed.
-	targetPath := filepath.ToSlash(filepath.Dir(filepath.Dir(realpath))) + "/" + strings.TrimSuffix(filepath.Base(realpath), filepath.Ext(filepath.Base(realpath)))
-	//log.Println(targetPath);
+	targetPath := filepath.ToSlash(filepath.Join(filepath.Dir(filepath.Dir(realpath)), strings.TrimSuffix(filepath.Base(realpath), filepath.Ext(filepath.Base(realpath)))))
+	//log.Println(targetPath)
 	os.Rename(realpath, targetPath)
 
 	//Check if the parent dir has no more fileds. If yes, remove it
@@ -1626,15 +1626,15 @@ func system_fs_handleOpr(w http.ResponseWriter, r *http.Request) {
 					return
 				}
 
-				thisFilename := newFilenames[i]
+				thisFilename := filepath.Base(newFilenames[i])
 				//Check if the name already exists. If yes, return false
-				if fileExists(filepath.Dir(rsrcFile) + "/" + thisFilename) {
+				if fileExists(filepath.Join(filepath.Dir(rsrcFile), thisFilename)) {
 					sendErrorResponse(w, "File already exists")
 					return
 				}
 
 				//Everything is ok. Rename the file.
-				targetNewName := filepath.Dir(rsrcFile) + "/" + thisFilename
+				targetNewName := filepath.Join(filepath.Dir(rsrcFile), thisFilename)
 				err = os.Rename(rsrcFile, targetNewName)
 				if err != nil {
 					sendErrorResponse(w, err.Error())

+ 1 - 1
src/main.flags.go

@@ -28,7 +28,7 @@ var subserviceBasePort = 12810            //Next subservice port
 
 // =========== SYSTEM BUILD INFORMATION ==============
 var build_version = "development"                      //System build flag, this can be either {development / production / stable}
-var internal_version = "0.1.121"                       //Internal build version, [fork_id].[major_release_no].[minor_release_no]
+var internal_version = "0.1.122"                       //Internal build version, [fork_id].[major_release_no].[minor_release_no]
 var deviceUUID string                                  //The device uuid of this host
 var deviceVendor = "IMUSLAB.INC"                       //Vendor of the system
 var deviceVendorURL = "http://imuslab.com"             //Vendor contact information

+ 1 - 1
src/mediaServer.go

@@ -55,7 +55,7 @@ func media_server_validateSourceFile(w http.ResponseWriter, r *http.Request) (st
 	}
 
 	targetfile, _ := mv(r, "file", false)
-	targetfile, _ = url.QueryUnescape(targetfile)
+	targetfile, err = url.QueryUnescape(targetfile)
 	if targetfile == "" {
 		return "", errors.New("Missing paramter 'file'")
 	}

+ 68 - 0
src/mod/filesystem/fileOpr.go

@@ -12,8 +12,10 @@ package filesystem
 */
 
 import (
+	"archive/tar"
 	"archive/zip"
 	"compress/flate"
+	"compress/gzip"
 	"errors"
 	"fmt"
 	"io"
@@ -751,3 +753,69 @@ func IsDir(path string) bool {
 	}
 	return false
 }
+
+//Unzip tar.gz file
+func ExtractTarGzipFile(filename string, outfile string) error {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+
+	err = ExtractTarGzipByStream(filepath.Clean(outfile), f, true)
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+func ExtractTarGzipByStream(basedir string, gzipStream io.Reader, onErrorResumeNext bool) error {
+	uncompressedStream, err := gzip.NewReader(gzipStream)
+	if err != nil {
+		return err
+	}
+
+	tarReader := tar.NewReader(uncompressedStream)
+
+	for {
+		header, err := tarReader.Next()
+
+		if err == io.EOF {
+			break
+		}
+
+		if err != nil {
+			return err
+		}
+
+		switch header.Typeflag {
+		case tar.TypeDir:
+			err := os.Mkdir(header.Name, 0755)
+			if err != nil {
+				if !onErrorResumeNext {
+					return err
+				}
+
+			}
+		case tar.TypeReg:
+			outFile, err := os.Create(filepath.Join(basedir, header.Name))
+			if err != nil {
+				if !onErrorResumeNext {
+					return err
+				}
+			}
+			_, err = io.Copy(outFile, tarReader)
+			if err != nil {
+				if !onErrorResumeNext {
+					return err
+				}
+			}
+			outFile.Close()
+
+		default:
+			//Unknown filetype, continue
+
+		}
+
+	}
+	return nil
+}

+ 3 - 0
src/mod/network/mdns/mdns.go

@@ -35,11 +35,14 @@ func NewMDNS(config NetworkHost) (*MDNSHost, error) {
 	macAddressBoardcast := ""
 	if err == nil {
 		macAddressBoardcast = strings.Join(macAddress, ",")
+	} else {
+		log.Println("[mDNS] Unable to get MAC Address: ", err.Error())
 	}
 
 	//Register the mds services
 	server, err := zeroconf.Register(config.HostName, "_http._tcp", "local.", config.Port, []string{"version_build=" + config.BuildVersion, "version_minor=" + config.MinorVersion, "vendor=" + config.Vendor, "model=" + config.Model, "uuid=" + config.UUID, "domain=" + config.Domain, "mac_addr=" + macAddressBoardcast}, nil)
 	if err != nil {
+		log.Println("[mDNS] Error when registering zeroconf broadcast message", err.Error())
 		return &MDNSHost{}, err
 	}
 

+ 1 - 1
src/mod/network/neighbour/neighbour.go

@@ -89,7 +89,7 @@ func (d *Discoverer) StartScanning(interval int, scanDuration int) {
 	//Update the Discoverer settings
 	d.d = done
 	d.t = ticker
-
+	log.Println("ArozOS Neighbour Scanning Completed, ", len(d.NearbyHosts), " neighbours found!")
 }
 
 func (d *Discoverer) UpdateScan(scanDuration int) {

+ 147 - 0
src/mod/network/netstat/netstat.go

@@ -0,0 +1,147 @@
+package netstat
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"imuslab.com/arozos/mod/common"
+)
+
+func HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
+	rx, tx, err := GetNetworkInterfaceStats()
+	if err != nil {
+		common.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	currnetNetSpec := struct {
+		RX int64
+		TX int64
+	}{
+		rx,
+		tx,
+	}
+
+	js, _ := json.Marshal(currnetNetSpec)
+	common.SendJSONResponse(w, string(js))
+}
+
+//Get network interface stats, return accumulated rx bits, tx bits and error if any
+func GetNetworkInterfaceStats() (int64, int64, error) {
+	if runtime.GOOS == "windows" {
+		cmd := exec.Command("wmic", "path", "Win32_PerfRawData_Tcpip_NetworkInterface", "Get", "BytesReceivedPersec,BytesSentPersec,BytesTotalPersec")
+		out, err := cmd.Output()
+		if err != nil {
+			return 0, 0, err
+		}
+
+		//Filter out the first line
+
+		lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n")
+		if len(lines) >= 2 && len(lines[1]) >= 0 {
+			dataLine := lines[1]
+			for strings.Contains(dataLine, "  ") {
+				dataLine = strings.ReplaceAll(dataLine, "  ", " ")
+			}
+			dataLine = strings.TrimSpace(dataLine)
+			info := strings.Split(dataLine, " ")
+			if len(info) < 3 {
+				return 0, 0, errors.New("Invalid wmic results")
+			}
+			rxString := info[0]
+			txString := info[1]
+
+			rx := int64(0)
+			tx := int64(0)
+			if s, err := strconv.ParseInt(rxString, 10, 64); err == nil {
+				rx = s
+			}
+
+			if s, err := strconv.ParseInt(txString, 10, 64); err == nil {
+				tx = s
+			}
+
+			//log.Println(rx, tx)
+			return rx * 4, tx * 4, nil
+		} else {
+			//Invalid data
+			return 0, 0, errors.New("Invalid wmic results")
+		}
+
+	} else if runtime.GOOS == "linux" {
+		allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
+		if err != nil {
+			//Permission denied
+			return 0, 0, errors.New("Access denied")
+		}
+
+		if len(allIfaceRxByteFiles) == 0 {
+			return 0, 0, errors.New("No valid iface found")
+		}
+
+		rxSum := int64(0)
+		txSum := int64(0)
+		for _, rxByteFile := range allIfaceRxByteFiles {
+			rxBytes, err := ioutil.ReadFile(rxByteFile)
+			if err == nil {
+				rxBytesInt, err := strconv.Atoi(strings.TrimSpace(string(rxBytes)))
+				if err == nil {
+					rxSum += int64(rxBytesInt)
+				}
+			}
+
+			//Usually the tx_bytes file is nearby it. Read it as well
+			txByteFile := filepath.Join(filepath.Dir(rxByteFile), "tx_bytes")
+			txBytes, err := ioutil.ReadFile(txByteFile)
+			if err == nil {
+				txBytesInt, err := strconv.Atoi(strings.TrimSpace(string(txBytes)))
+				if err == nil {
+					txSum += int64(txBytesInt)
+				}
+			}
+
+		}
+
+		//Return value as bits
+		return rxSum * 8, txSum * 8, nil
+
+	} else if runtime.GOOS == "darwin" {
+		cmd := exec.Command("netstat", "-ib") //get data from netstat -ib
+		out, err := cmd.Output()
+		if err != nil {
+			return 0, 0, err
+		}
+
+		outStrs := string(out)                                                          //byte array to multi-line string
+		for _, outStr := range strings.Split(strings.TrimSuffix(outStrs, "\n"), "\n") { //foreach multi-line string
+			if strings.HasPrefix(outStr, "en") { //search for ethernet interface
+				if strings.Contains(outStr, "<Link#") { //search for the link with <Link#?>
+					outStrSplit := strings.Fields(outStr) //split by white-space
+
+					rxSum, errRX := strconv.Atoi(outStrSplit[6]) //received bytes sum
+					if errRX != nil {
+						return 0, 0, errRX
+					}
+
+					txSum, errTX := strconv.Atoi(outStrSplit[9]) //transmitted bytes sum
+					if errTX != nil {
+						return 0, 0, errTX
+					}
+
+					return int64(rxSum) * 8, int64(txSum) * 8, nil
+				}
+			}
+		}
+
+		return 0, 0, nil //no ethernet adapters with en*/<Link#*>
+	}
+
+	return 0, 0, errors.New("Platform not supported")
+}

+ 71 - 0
src/mod/notification/notification.go

@@ -0,0 +1,71 @@
+package main
+
+import "container/list"
+
+/*
+	Notification Producer and Consumer Queue
+
+	This module is designed to route the notification from module that produce it
+	to all the devices or agent that can reach the user
+*/
+
+type NotificationPayload struct {
+	ID        string   //Notification ID, generate by producer
+	Title     string   //Title of the notification
+	Message   string   //Message of the notification
+	Receiver  []string //Receiver, username in arozos system
+	Sender    string   //Sender, the sender or module of the notification
+	ActionURL string   //URL for futher action or open related pages (as url), leave empty if not appliable
+	IsUrgent  bool     //Label this notification as urgent
+}
+
+//Notification Consumer, object that use to consume notification from queue
+type Consumer struct {
+	Name string
+	Desc string
+
+	ListenTopicMode int
+	Notify          func(*NotificationPayload) error
+	ListeningQueue  *NotificationQueue
+}
+
+//Notification Producer, object that use to create and push notification into the queue
+type Producer struct {
+	Name string
+	Desc string
+
+	PushTopicType int
+	TargetQueue   *NotificationQueue
+}
+
+type NotificationQueue struct {
+	Producers []*Producer
+	Consumers []*Consumer
+
+	MasterQueue *list.List
+}
+
+func NewNotificationQueue() *NotificationQueue {
+	thisQueue := list.New()
+
+	return &NotificationQueue{
+		Producers:   []*Producer{},
+		Consumers:   []*Consumer{},
+		MasterQueue: thisQueue,
+	}
+}
+
+//Add a notification producer into the master queue
+func (n *NotificationQueue) AddNotificationProducer(p *Producer) {
+	n.Producers = append(n.Producers, p)
+}
+
+//Add a notification consumer into the master queue
+func (n *NotificationQueue) AddNotificationConsumer(c *Consumer) {
+	n.Consumers = append(n.Consumers, c)
+}
+
+//Push a notifiation to all consumers with same topic type
+func (n *NotificationQueue) PushNotification(TopicType int, message *NotificationPayload) {
+
+}

+ 3 - 0
src/network.go

@@ -7,6 +7,7 @@ import (
 
 	network "imuslab.com/arozos/mod/network"
 	mdns "imuslab.com/arozos/mod/network/mdns"
+	"imuslab.com/arozos/mod/network/netstat"
 	ssdp "imuslab.com/arozos/mod/network/ssdp"
 	upnp "imuslab.com/arozos/mod/network/upnp"
 	prout "imuslab.com/arozos/mod/prouter"
@@ -51,6 +52,8 @@ func NetworkServiceInit() {
 		})
 	}
 
+	router.HandleFunc("/system/network/getNICUsage", netstat.HandleGetNetworkInterfaceStats)
+
 	//Start the services that depends on network interface
 	StartNetworkServices()
 

+ 22 - 0
src/startup.go

@@ -7,12 +7,34 @@ package main
 
 import (
 	"fmt"
+	"log"
+	"os"
 
 	db "imuslab.com/arozos/mod/database"
+	"imuslab.com/arozos/mod/filesystem"
 )
 
 func RunStartup() {
 	//1. Initiate the main system database
+
+	//Check if system or web both not exists and web.tar.gz exists. Unzip it for the user
+	if (!fileExists("system/") || !fileExists("web/")) && fileExists("./web.tar.gz") {
+		log.Println("Unzipping system critical files from archive")
+		extErr := filesystem.ExtractTarGzipFile("./web.tar.gz", "./")
+		if extErr != nil {
+			//Extract failed
+			fmt.Println("▒▒ ERROR: UNABLE TO EXTRACT CRITICAL SYSTEM FOLDERS ▒▒")
+			fmt.Println(extErr)
+			panic("Unable to extract content from web.tar.gz to fix the missing system / web folder. Please unzip the web.tar.gz manually.")
+		}
+
+		//Extract success
+		extErr = os.Remove("./web.tar.gz")
+		if extErr != nil {
+			log.Println("Unable to remove web.tar.gz: ", extErr)
+		}
+	}
+
 	if !fileExists("system/") {
 		fmt.Println("▒▒ ERROR: SYSTEM FOLDER NOT FOUND ▒▒")
 		panic("This error occurs because the system folder is missing. Please follow the installation guide and don't just download a binary and run it.")

+ 1 - 1
src/system/storage.json.example

@@ -45,4 +45,4 @@
     "hierarchy":"public",
     "automount":false
   }
-]
+]

+ 13 - 12
src/user.go

@@ -29,17 +29,19 @@ func UserSystemInit() {
 	}
 	userHandler = uh
 
-	router := prout.NewModuleRouter(prout.RouterOption{
-		ModuleName:  "System Settings",
-		AdminOnly:   false,
-		UserHandler: userHandler,
-		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
-			sendErrorResponse(w, "Permission Denied")
-		},
-	})
+	/*
+		router := prout.NewModuleRouter(prout.RouterOption{
+			ModuleName:  "System Settings",
+			AdminOnly:   false,
+			UserHandler: userHandler,
+			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+				sendErrorResponse(w, "Permission Denied")
+			},
+		})
+	*/
 
 	//Create Endpoint Listeners
-	router.HandleFunc("/system/users/list", user_handleList)
+	http.HandleFunc("/system/users/list", user_handleList)
 
 	//Everyone logged in should have permission to view their profile and change their password
 	http.HandleFunc("/system/users/userinfo", func(w http.ResponseWriter, r *http.Request) {
@@ -373,7 +375,7 @@ func user_handleList(w http.ResponseWriter, r *http.Request) {
 		sendErrorResponse(w, "User not logged in")
 		return
 	}
-	if userinfo.IsAdmin() == true {
+	if authAgent.CheckAuth(r) {
 		entries, _ := sysdb.ListTable("auth")
 		var results [][]interface{}
 		for _, keypairs := range entries {
@@ -396,8 +398,7 @@ func user_handleList(w http.ResponseWriter, r *http.Request) {
 		jsonString, _ := json.Marshal(results)
 		sendJSONResponse(w, string(jsonString))
 	} else {
-		sendErrorResponse(w, "Permission denied")
-		return
+		sendErrorResponse(w, "Permission Denied")
 	}
 }
 

+ 201 - 0
src/web/SystemAO/info/taskManager.html

@@ -68,10 +68,37 @@
            
            
         </div>
+        <div id="netChartContainer" style="position: relative; margin-top: 1.2em;">
+            <h2 class="ui header">
+                Network
+                <div class="sub header">Network usage in the previous 60 seconds</div>
+            </h2>
+            <p id="netGraphScale" style="position: absolute; top: 1em; right: 0.3em; font-size: 16px;">100 kbps</p>
+            <canvas id="netChart" width="1200" height="300"></canvas>
+            <div class="ui stackable grid">
+                <div class="four wide column">
+                    <div class="ui header" style="border-left: 2px solid #bc793f; padding-left: 1em;">
+                        <span id="rx">Loading</span>
+                         <div class="sub header">Received</div>
+                     </div>
+                </div>
+                <div class="four wide column">
+                    <div class="ui header"  style="border-left: 2px dotted #bc793f; padding-left: 1em;">
+                        <span id="tx">Loading</span>
+                        <div class="sub header">Transmitted</div>
+                     </div>
+                </div>
+            </div>
+           
+        </div>
+
+        <br><br>
     </div>
     <script>
         var cpuChart;
         var ramChart;
+        var netChart;
+        var previousNetData = [0, 0];
 
         //Override Chart.js v3 poor API designs
         Chart.defaults.plugins.tooltip.enabled = false;
@@ -102,6 +129,9 @@
                     max: 100,
                     grid: {
                         color:  "rgba(83, 160, 205, 0.2)"
+                    },
+                    ticks: {
+                        display: false,
                     }
                 }
             },
@@ -142,6 +172,52 @@
                     max: 100,
                     grid: {
                         color:  "rgba(156, 55, 185, 0.2)"
+                    },
+                    ticks: {
+                        display: false,
+                    }
+                }
+            },
+            legend: {
+                display: false,
+            },
+            tooltips: {
+                callbacks: {
+                    label: function(tooltipItem) {
+                            return tooltipItem.yLabel;
+                    }
+                }
+            }
+        };
+
+        var netOptions = {
+            maintainAspectRatio: true,
+            responsive: true,
+			spanGaps: false,
+			elements: {
+				line: {
+					tension: 0.000001
+				}
+			},
+			plugins: {
+				filler: {
+					propagate: false
+				},
+			},
+			scales: {
+				x: {
+                    grid: {
+                        color:  "rgba(167, 79, 1, 0.2)"
+                    }
+                },
+                y: {
+                    min: Math.min.apply(this, getMergedRxTxDataset()),
+                    max: Math.max.apply(this, getMergedRxTxDataset()) + 5,
+                    grid: {
+                        color:  "rgba(167, 79, 1, 0.2)"
+                    },
+                    ticks: {
+                        display: false,
                     }
                 }
             },
@@ -195,6 +271,14 @@
             return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
         }
 
+        function bitToSize(bytes) {
+            var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb'];
+            if (bytes == 0) return '0 b';
+            var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)));
+            return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
+        }
+
+
         function chartInit(){
             cpuChart = new Chart('cpuChart', {
 				type: 'line',
@@ -204,6 +288,8 @@
 						backgroundColor: "rgba(241,246,250,0.4)",
 						borderColor: "#4c9dcb",
 						data: [],
+                        radius: 0,
+                        borderWidth: 2,
 						fill: 'start'
                     }]
 				},
@@ -224,6 +310,8 @@
 						backgroundColor: "rgba(244,242,244,0.4)",
 						borderColor: "#9528b4",
 						data: [],
+                        radius: 0,
+                        borderWidth: 2,
 						fill: 'start'
                     }]
 				},
@@ -233,6 +321,37 @@
             for (var i =0; i < 60; i++){
                 addData(ramChart, "",0)
             }
+
+            //Create Network Chart
+            netChart = new Chart('netChart', {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						backgroundColor: "rgba(252,243,235,0.4)",
+						borderColor: "#a74f01",
+						data: [],
+                        radius: 0,
+                        borderWidth: 2,
+						fill: 'start'
+                    },
+                    {
+						backgroundColor: "rgba(252,243,235,0.2)",
+						borderColor: "#a74f01",
+                        borderDash: [3, 3],
+						data: [],
+                        radius: 0,
+                        borderWidth: 2,
+						fill: 'start'
+                        
+                    }]
+				},
+				options: netOptions
+            });
+
+            for (var i =0; i < 60; i++){
+                addNetData(netChart, "", 0, 0)
+            }
             
         }
 
@@ -256,6 +375,50 @@
             }, 1000)
             */
 
+             //Calculate the bandwidth diff
+            $.get("../../system/network/getNICUsage", function(data){
+                if (data.error !== undefined){
+                    //Error
+                    console.log(data.error);
+                    $("#netGraphScale").text(data.error);
+                    return;
+                }
+                if (previousNetData[0] == 0 && previousNetData[1] == 0){
+                    //Not initiated. Set base and wait for next iteration
+                    previousNetData = [data.RX, data.TX];
+                }else{
+                    var rxd = data.RX - previousNetData[0];
+                    var txd = data.TX - previousNetData[1];
+                    previousNetData = [data.RX, data.TX];
+                    addAndShiftNetworkData(netChart, "", rxd, txd);
+
+                    $("#rx").text(bitToSize(rxd)+"/s");
+                    $("#tx").text(bitToSize(txd)+"/s");
+
+                    //Get the max value of the diagram, round it to the cloest 10x
+                    var chartMaxValue = Math.max.apply(this, getMergedRxTxDataset()) * 1.2;
+
+                    //Special Rounding for calculating graph scale
+                    baseValue = parseInt(chartMaxValue);
+                    var scale = "0 bps"
+                    var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb'];
+                    function roundUpNearest(num) {
+                        return Math.ceil(num / 10) * 10;
+                    }
+
+                    if (baseValue == 0){
+
+                    }else{
+                        var i = parseInt(Math.floor(Math.log(baseValue) / Math.log(1000)));
+                        scale = roundUpNearest((baseValue / Math.pow(1024, i)).toFixed(0))
+                        scale += ' ' + sizes[i] + "ps";
+                    }
+                    
+                    //console.log(baseValue, chartMaxValue, scale);
+                    $("#netGraphScale").text(scale);
+                }
+            })
+
             $.get("../../system/info/getUsageInfo", function(data){
                 //Update graph
                 addAndShiftChartDate(cpuChart, "", data.CPU);
@@ -274,6 +437,13 @@
 
         }
 
+        function addNetData(chart, label, rx, tx) {
+            chart.data.labels.push(label);
+            chart.data.datasets[0].data.push(rx);
+            chart.data.datasets[1].data.push(tx);
+            chart.update();
+        }
+
         function addData(chart, label, data) {
             chart.data.labels.push(label);
             chart.data.datasets.forEach((dataset) => {
@@ -299,6 +469,37 @@
             chart.update();
         }
 
+        function addAndShiftNetworkData(chart, label, rxd, txd) {
+            chart.data.labels.splice(0, 1); // remove first label
+            chart.data.datasets.forEach(function(dataset) {
+                dataset.data.splice(0, 1); // remove first data point
+            });
+
+            chart.update();
+
+            // Add new data
+            chart.data.labels.push(label); // add new label at end
+            chart.data.datasets[0].data.push(rxd);
+            chart.data.datasets[1].data.push(txd);
+            
+            
+
+            //Update the sacle as well
+            netChart.options.scales.y.min = Math.min.apply(this, getMergedRxTxDataset());
+            netChart.options.scales.y.max = Math.max.apply(this, getMergedRxTxDataset()) *1.2;
+
+            chart.update();
+        }
+
+        function getMergedRxTxDataset(){
+            if (netChart == undefined){
+                return [0, 100];
+            }
+            var newArr = [];
+            newArr = newArr.concat(netChart.data.datasets[0].data,netChart.data.datasets[1].data);
+            return newArr;
+        }
+
     </script>
 </body>
 </html>

+ 12 - 3
src/web/SystemAO/modules/moduleList.html

@@ -30,7 +30,7 @@
               </table>
               <div class="ui divider"></div>
               <p>If you have installed WebApps manually, you can click the "Reload WebApps" button to load it without restarting ArozOS.</p>
-              <button class="ui basic small blue button" onclick="reloadWebapps();">
+              <button id="reloadWebappButton" class="ui basic small blue button" onclick="reloadWebapps();">
                 <i class="refresh icon"></i> Reload WebApps
               </button>
               <br><br>
@@ -39,9 +39,18 @@
             initModuleList();
 
             function reloadWebapps(){
+                let moduleListBackup = $("#moduleList").html();
                 $("#moduleList").html(`<tr><td colspan="6"><i class="ui loading spinner icon"></i> Reloading...</tr></td>`);
-                $.get("../../system/modules/reload", function(data){
-                    initModuleList();
+                $.ajax({
+                   url: "../../system/modules/reload", 
+                   success: function(data){
+                        initModuleList();
+                   },
+                   error: function(){
+                       //Reload failed (Permission denied?)
+                       $("#moduleList").html(moduleListBackup);
+                       $("#reloadWebappButton").addClass("disabled").html("<i class='ui remove icon'></i> No Permission");
+                   }    
                 });
             }
 

+ 10 - 4
src/web/desktop.system

@@ -1226,10 +1226,7 @@
                     alert(data.error);
                 }else{
                     userInfo = data;
-                    if (data.IsAdmin == false){
-                        //Hide the power buttons
-                        $(".hardware").hide();
-                    }
+                   
 
                     //Update the user tag
                     $("#username").text(userInfo.Username);
@@ -1238,6 +1235,14 @@
                     if (data.UserIcon !== ""){
                         $(".usericon").attr("src",data.UserIcon);
                     }
+
+                    if (data.IsAdmin == false){
+                        //Hide the power buttons
+                        $(".hardware").hide();
+                    }else{
+                        //User is admin. Add admin icon
+                        $("#username").append(` <i style="color: #52c9ff" class="protect icon themed text"></i>`);
+                    }
                 }
             });
         }
@@ -6261,6 +6266,7 @@
             $("#listMenu").find(".searchBar").css("border-bottom", "2px solid " + newThemeColor);
             $("#volumebar").css("background-color", newThemeColor);
             $("#brightnessbar").css("background-color", newThemeColor);
+            $(".themed.text").css("color", newThemeColor);
 
             //Connection lost notification css
             $("#connectionLost").find(".ts.card").css("background-color", hexToRgbA(newThemeColor, 0.5));