Przeglądaj źródła

Added wip mdadm ui

TC 2 tygodni temu
rodzic
commit
d79106bece

+ 3 - 109
api.go

@@ -9,7 +9,6 @@ import (
 
 	"imuslab.com/bokofs/bokofsd/mod/diskinfo"
 	"imuslab.com/bokofs/bokofsd/mod/diskinfo/lsblk"
-	"imuslab.com/bokofs/bokofsd/mod/diskinfo/smart"
 	"imuslab.com/bokofs/bokofsd/mod/netstat"
 )
 
@@ -44,114 +43,9 @@ func HandlerAPIcalls() http.Handler {
 			// Request to /api/smart/*
 			HandleSMARTCalls().ServeHTTP(w, r)
 			return
-		default:
-			http.Error(w, "Not Found", http.StatusNotFound)
-			return
-		}
-	}))
-}
-
-// Handler for SMART API calls
-func HandleSMARTCalls() http.Handler {
-	return http.StripPrefix("/smart/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		pathParts := strings.Split(r.URL.Path, "/")
-		if len(pathParts) < 2 {
-			http.Error(w, "Bad Request - Missing disk name", http.StatusBadRequest)
-			return
-		}
-		subPath := pathParts[0]
-		diskName := pathParts[1]
-		if diskName == "" {
-			http.Error(w, "Bad Request - Missing disk name", http.StatusBadRequest)
-			return
-		}
-		switch subPath {
-		case "health":
-			if diskName == "all" {
-				// Get the SMART information for all disks
-				allDisks, err := diskinfo.GetAllDisks()
-				if err != nil {
-					log.Println("Error getting all disks:", err)
-					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-					return
-				}
-
-				// Create a map to hold the SMART information for each disk
-				diskInfoMap := []*smart.DriveHealthInfo{}
-				for _, disk := range allDisks {
-					diskName := disk.Name
-					health, err := smart.GetDiskSMARTHealthSummary(diskName)
-					if err != nil {
-						log.Println("Error getting disk health:", err)
-						continue
-					}
-
-					diskInfoMap = append(diskInfoMap, health)
-				}
-				// Convert the disk information to JSON and write it to the response
-				js, _ := json.Marshal(diskInfoMap)
-				w.Header().Set("Content-Type", "application/json")
-				w.WriteHeader(http.StatusOK)
-				w.Write(js)
-				return
-			}
-
-			// Get the health status of the disk
-			health, err := smart.GetDiskSMARTHealthSummary(diskName)
-			if err != nil {
-				log.Println("Error getting disk health:", err)
-				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-				return
-			}
-			// Convert the health status to JSON and write it to the response
-			js, _ := json.Marshal(health)
-			w.Header().Set("Content-Type", "application/json")
-			w.WriteHeader(http.StatusOK)
-			w.Write(js)
-			return
-		case "info":
-			// Handle SMART API calls
-			dt, err := smart.GetDiskType(diskName)
-			if err != nil {
-				log.Println("Error getting disk type:", err)
-				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-				return
-			}
-
-			if dt == smart.DiskType_SATA {
-				// Get SATA disk information
-				sataInfo, err := smart.GetSATAInfo(diskName)
-				if err != nil {
-					log.Println("Error getting SATA disk info:", err)
-					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-					return
-				}
-
-				// Convert the SATA info to JSON and write it to the response
-				js, _ := json.Marshal(sataInfo)
-				w.Header().Set("Content-Type", "application/json")
-				w.WriteHeader(http.StatusOK)
-				w.Write(js)
-			} else if dt == smart.DiskType_NVMe {
-				// Get NVMe disk information
-				nvmeInfo, err := smart.GetNVMEInfo(diskName)
-				if err != nil {
-					log.Println("Error getting NVMe disk info:", err)
-					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-					return
-				}
-
-				// Convert the NVMe info to JSON and write it to the response
-				js, _ := json.Marshal(nvmeInfo)
-				w.Header().Set("Content-Type", "application/json")
-				w.WriteHeader(http.StatusOK)
-				w.Write(js)
-			} else {
-				log.Println("Unknown disk type:", dt)
-				http.Error(w, "Bad Request - Unknown disk type", http.StatusBadRequest)
-				return
-			}
-			return
+		case "raid":
+			// Request to /api/raid/*
+			HandleRAIDCalls().ServeHTTP(w, r)
 		default:
 			http.Error(w, "Not Found", http.StatusNotFound)
 			return

+ 1 - 1
check.go

@@ -14,7 +14,7 @@ func commandExists(cmd string) bool {
 // checkRuntimeEnvironment checks if the required commands are available in the runtime environment
 func checkRuntimeEnvironment() bool {
 	packageMissing := false
-	commands := []string{"ffmpeg", "smartctl", "lsblk", "blkid", "df"}
+	commands := []string{"ffmpeg", "smartctl", "mdadm", "lsblk", "blkid", "df"}
 	for _, cmd := range commands {
 		if commandExists(cmd) {
 			fmt.Printf("\033[32m✔\033[0m '%s' exists\n", cmd)

+ 1 - 0
config/sys.uuid

@@ -0,0 +1 @@
+87ae7c7a-a3e7-4324-85e5-072280c02e3b

+ 17 - 4
def.go

@@ -2,17 +2,30 @@ package main
 
 import (
 	"flag"
+	"net/http"
 
+	"imuslab.com/bokofs/bokofsd/mod/disktool/raid"
 	"imuslab.com/bokofs/bokofsd/mod/netstat"
 )
 
+const (
+	CSRF_COOKIENAME = "bokofs-csrf"
+)
+
 var (
 	/* Start Flags */
-	httpPort    = flag.Int("p", 9000, "Port to serve on (Plain HTTP)")
-	devMode     = flag.Bool("dev", false, "Enable development mode")
-	config      = flag.String("c", "./config", "Path to the config folder")
-	serveSecure = flag.Bool("s", false, "Serve HTTPS. Default false")
+	httpPort = flag.Int("p", 9000, "Port to serve on (Plain HTTP)")
+	devMode  = flag.Bool("dev", false, "Enable development mode")
+	config   = flag.String("c", "./config", "Path to the config folder")
+
+	//serveSecure = flag.Bool("s", false, "Serve HTTPS. Default false")
 
 	/* Runtime Variables */
+	sysuuid        string                          //System UUID (UUIDv4)
+	webfs          http.FileSystem                 //The web filesystem for static files
+	csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
+
+	/* Modules */
 	netstatBuffer *netstat.NetStatBuffers
+	raidManager   *raid.Manager
 )

+ 3 - 0
go.mod

@@ -6,6 +6,8 @@ require (
 	github.com/anatol/smart.go v0.0.0-20241126061019-f03d79b340d2
 	github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
 	github.com/fogleman/fauxgl v0.0.0-20250110135958-abf826acbbbd
+	github.com/google/uuid v1.6.0
+	github.com/gorilla/csrf v1.7.2
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
 	github.com/oliamb/cutter v0.2.2
 	github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb
@@ -18,6 +20,7 @@ require (
 	github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
+	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	golang.org/x/crypto v0.33.0 // indirect

+ 8 - 0
go.sum

@@ -14,8 +14,16 @@ github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 h1:n3RPbpwXSFT0G
 github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046/go.mod h1:KDwyDqFmVUxUmo7tmqXtyaaJMdGon06y8BD2jmh84CQ=
 github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
 github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
+github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
+github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=

+ 17 - 42
main.go

@@ -4,13 +4,13 @@ import (
 	"embed"
 	"flag"
 	"fmt"
-	"io/fs"
 	"net/http"
 	"os"
+	"os/signal"
+	"syscall"
 
 	"imuslab.com/bokofs/bokofsd/mod/bokofs"
 	"imuslab.com/bokofs/bokofsd/mod/bokofs/bokoworker"
-	"imuslab.com/bokofs/bokofsd/mod/netstat"
 )
 
 //go:embed web/*
@@ -19,47 +19,21 @@ var embeddedFiles embed.FS
 func main() {
 	flag.Parse()
 
-	/* File system handler */
-	var fileSystem http.FileSystem
-	if *devMode {
-		fmt.Println("Development mode enabled. Serving files from ./web directory.")
-		fileSystem = http.Dir("./web")
-	} else {
-		fmt.Println("Production mode enabled. Serving files from embedded filesystem.")
-		subFS, err := fs.Sub(embeddedFiles, "web")
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error accessing embedded subdirectory: %v\n", err)
-			os.Exit(1)
-		}
-		fileSystem = http.FS(subFS)
-	}
-
-	configFolderPath := "./config"
-	if *config != "" {
-		configFolderPath = *config
-	}
-	if _, err := os.Stat(configFolderPath); os.IsNotExist(err) {
-		fmt.Printf("Config folder does not exist. Creating folder at %s\n", configFolderPath)
-		if err := os.Mkdir(configFolderPath, os.ModePerm); err != nil {
-			fmt.Fprintf(os.Stderr, "Error creating config folder: %v\n", err)
-			os.Exit(1)
-		}
-	}
-
-	/* Network statistics */
-	nsb, err := netstat.NewNetStatBuffer(300)
+	// Start the application
+	err := initialization()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error creating network statistics buffer: %v\n", err)
-		os.Exit(1)
+		panic(err)
 	}
-	defer netstatBuffer.Close()
-	netstatBuffer = nsb
 
-	/* Package Check */
-	if !checkRuntimeEnvironment() {
-		fmt.Println("Runtime environment check failed. Please install the missing packages.")
-		os.Exit(1)
-	}
+	// Capture termination signals and call cleanup
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-signalChan
+		fmt.Println("Received termination signal, cleaning up...")
+		cleanup()
+		os.Exit(0)
+	}()
 
 	//DEBUG
 	wds, err := bokofs.NewWebdavInterfaceServer("/disk/", "/thumb/")
@@ -89,7 +63,8 @@ func main() {
 
 	//END DEBUG
 
-	http.Handle("/", http.FileServer(fileSystem))
+	/* Static Web Server */
+	http.Handle("/", csrfMiddleware(http.FileServer(webfs)))
 
 	/* WebDAV Handlers */
 	http.Handle("/disk/", wds.FsHandler())     //Note the trailing slash
@@ -101,7 +76,7 @@ func main() {
 		fmt.Fprintln(w, "Meta handler not implemented yet")
 	}))
 
-	http.Handle("/api/", HandlerAPIcalls())
+	http.Handle("/api/", csrfMiddleware(HandlerAPIcalls()))
 
 	addr := fmt.Sprintf(":%d", *httpPort)
 	fmt.Printf("Starting static web server on %s\n", addr)

+ 22 - 0
mod/disktool/raid/handler.go

@@ -679,3 +679,25 @@ func (m *Manager) HandleRenderOverview(w http.ResponseWriter, r *http.Request) {
 	js, _ := json.Marshal(results)
 	utils.SendJSONResponse(w, string(js))
 }
+
+func (m *Manager) HandleGetRAIDSyncState(w http.ResponseWriter, r *http.Request) {
+	devName, err := utils.GetPara(r, "dev")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid device name given")
+		return
+	}
+
+	if !strings.HasPrefix(devName, "/dev/") {
+		devName = filepath.Join("/dev/", devName)
+	}
+
+	//Get the sync state of the RAID device
+	syncState, err := m.GetSyncStateByPath(devName)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(syncState)
+	utils.SendJSONResponse(w, string(js))
+}

+ 104 - 0
mod/disktool/raid/sync.go

@@ -0,0 +1,104 @@
+package raid
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+)
+
+type SyncState struct {
+	DeviceName      string  //e.g, md0
+	ResyncPercent   float64 //e.g, 0.5
+	CompletedBlocks int64
+	TotalBlocks     int64
+	ExpectedTime    string //e.g, 1h23m
+	Speed           string //e.g, 1234K/s
+}
+
+// GetSyncStateByPath retrieves the synchronization state of a RAID device by its device path.
+// devpath can be either a full path (e.g., /dev/md0) or just the device name (e.g., md0).
+func (m *Manager) GetSyncStateByPath(devpath string) (*SyncState, error) {
+	devpath = strings.TrimPrefix(devpath, "/dev/")
+
+	syncStates, err := m.GetSyncStates()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, syncState := range syncStates {
+		if syncState.DeviceName == devpath {
+			return &syncState, nil
+		}
+	}
+
+	return nil, errors.New("device not found")
+}
+
+// GetSyncStates retrieves the synchronization states of RAID arrays from /proc/mdstat.
+func (m *Manager) GetSyncStates() ([]SyncState, error) {
+	file, err := os.Open("/proc/mdstat")
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	var syncStates []SyncState
+	var lastDeviceName string = ""
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.Contains(line, "resync =") || strings.Contains(line, "recovery =") {
+			parts := strings.Fields(line)
+			var syncState SyncState
+
+			for i, part := range parts {
+				if part == "resync" || part == "recovery" {
+					// Extract percentage
+					if i+2 < len(parts) && strings.HasSuffix(parts[i+2], "%") {
+						fmt.Sscanf(parts[i+2], "%f%%", &syncState.ResyncPercent)
+					}
+
+					// Extract completed and total blocks
+					if i+3 < len(parts) && strings.HasPrefix(parts[i+3], "(") && strings.Contains(parts[i+3], "/") {
+						var completed, total int64
+						fmt.Sscanf(parts[i+3], "(%d/%d)", &completed, &total)
+						syncState.CompletedBlocks = completed
+						syncState.TotalBlocks = total
+					}
+
+					// Extract expected time
+					if i+4 < len(parts) && strings.HasPrefix(parts[i+4], "finish=") {
+						syncState.ExpectedTime = strings.TrimPrefix(parts[i+4], "finish=")
+					}
+
+					// Extract speed
+					if i+5 < len(parts) && strings.HasPrefix(parts[i+5], "speed=") {
+						syncState.Speed = strings.TrimPrefix(parts[i+5], "speed=")
+					}
+				}
+			}
+
+			syncState.DeviceName = lastDeviceName
+			if syncState.DeviceName == "" {
+				return nil, errors.New("device name not found")
+			}
+
+			// Add the sync state to the list
+			syncStates = append(syncStates, syncState)
+		} else if strings.HasPrefix(line, "md") {
+			// Extract device name
+			parts := strings.Fields(line)
+			if len(parts) > 0 {
+				lastDeviceName = strings.TrimPrefix(parts[0], "/dev/")
+			}
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	return syncStates, nil
+}

+ 45 - 0
raid.go

@@ -0,0 +1,45 @@
+package main
+
+import (
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"imuslab.com/bokofs/bokofsd/mod/utils"
+)
+
+/*
+	raid.go
+
+	This file handles the RAID management and monitoring API routing
+*/
+
+func HandleRAIDCalls() http.Handler {
+	return http.StripPrefix("/raid/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		pathParts := strings.Split(r.URL.Path, "/")
+
+		switch pathParts[0] {
+		case "list":
+			// List all RAID devices
+			raidManager.HandleListRaidDevices(w, r)
+			return
+		case "sync":
+			// Get the RAID sync state, require "dev=md0" as a query parameter
+			raidManager.HandleGetRAIDSyncState(w, r)
+			return
+		case "test":
+			ss, err := raidManager.GetSyncStates()
+			if err != nil {
+				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+				return
+			}
+
+			js, _ := json.Marshal(ss)
+			utils.SendJSONResponse(w, string(js))
+			return
+		default:
+			http.Error(w, "Not Found", http.StatusNotFound)
+			return
+		}
+	}))
+}

+ 124 - 0
smart.go

@@ -0,0 +1,124 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"strings"
+
+	"imuslab.com/bokofs/bokofsd/mod/diskinfo"
+	"imuslab.com/bokofs/bokofsd/mod/diskinfo/smart"
+	"imuslab.com/bokofs/bokofsd/mod/utils"
+)
+
+/*
+	smart.go
+
+	This file handles the SMART management and monitoring API routing
+
+	Support APIs
+
+	/smart/health/{diskname} - Get the health status of a disk
+	/smart/health/all - Get the health status of all disks
+	/smart/info/{diskname} - Get the SMART information of a disk
+*/
+
+// Handler for SMART API calls
+func HandleSMARTCalls() http.Handler {
+	return http.StripPrefix("/smart/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		pathParts := strings.Split(r.URL.Path, "/")
+		if len(pathParts) < 2 {
+			http.Error(w, "Bad Request - Missing disk name", http.StatusBadRequest)
+			return
+		}
+		subPath := pathParts[0]
+		diskName := pathParts[1]
+		if diskName == "" {
+			http.Error(w, "Bad Request - Missing disk name", http.StatusBadRequest)
+			return
+		}
+		switch subPath {
+		case "health":
+			if diskName == "all" {
+				// Get the SMART information for all disks
+				allDisks, err := diskinfo.GetAllDisks()
+				if err != nil {
+					log.Println("Error getting all disks:", err)
+					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+					return
+				}
+
+				// Create a map to hold the SMART information for each disk
+				diskInfoMap := []*smart.DriveHealthInfo{}
+				for _, disk := range allDisks {
+					diskName := disk.Name
+					health, err := smart.GetDiskSMARTHealthSummary(diskName)
+					if err != nil {
+						log.Println("Error getting disk health:", err)
+						continue
+					}
+
+					diskInfoMap = append(diskInfoMap, health)
+				}
+				// Convert the disk information to JSON and write it to the response
+				js, _ := json.Marshal(diskInfoMap)
+				utils.SendJSONResponse(w, string(js))
+				return
+			}
+
+			// Get the health status of the disk
+			health, err := smart.GetDiskSMARTHealthSummary(diskName)
+			if err != nil {
+				log.Println("Error getting disk health:", err)
+				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+				return
+			}
+			// Convert the health status to JSON and write it to the response
+			js, _ := json.Marshal(health)
+			utils.SendJSONResponse(w, string(js))
+			return
+		case "info":
+			// Handle SMART API calls
+			dt, err := smart.GetDiskType(diskName)
+			if err != nil {
+				log.Println("Error getting disk type:", err)
+				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+				return
+			}
+
+			if dt == smart.DiskType_SATA {
+				// Get SATA disk information
+				sataInfo, err := smart.GetSATAInfo(diskName)
+				if err != nil {
+					log.Println("Error getting SATA disk info:", err)
+					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+					return
+				}
+
+				// Convert the SATA info to JSON and write it to the response
+				js, _ := json.Marshal(sataInfo)
+				utils.SendJSONResponse(w, string(js))
+			} else if dt == smart.DiskType_NVMe {
+				// Get NVMe disk information
+				nvmeInfo, err := smart.GetNVMEInfo(diskName)
+				if err != nil {
+					log.Println("Error getting NVMe disk info:", err)
+					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+					return
+				}
+
+				// Convert the NVMe info to JSON and write it to the response
+				js, _ := json.Marshal(nvmeInfo)
+				utils.SendJSONResponse(w, string(js))
+			} else {
+				log.Println("Unknown disk type:", dt)
+				http.Error(w, "Bad Request - Unknown disk type", http.StatusBadRequest)
+				return
+			}
+			return
+		default:
+			http.Error(w, "Not Found", http.StatusNotFound)
+			return
+		}
+	}))
+}

+ 103 - 0
start.go

@@ -0,0 +1,103 @@
+package main
+
+import (
+	"fmt"
+	"io/fs"
+	"net/http"
+	"os"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/csrf"
+	"imuslab.com/bokofs/bokofsd/mod/disktool/raid"
+	"imuslab.com/bokofs/bokofsd/mod/netstat"
+)
+
+/*
+	start.go
+
+	This file handles the startup and initialization of the application
+*/
+
+func initialization() error {
+	/* Check and generate system UUID */
+	configFolderPath := "./config"
+	if _, err := os.Stat(configFolderPath); os.IsNotExist(err) {
+		fmt.Printf("Config folder does not exist. Creating folder at %s\n", configFolderPath)
+		if err := os.Mkdir(configFolderPath, os.ModePerm); err != nil {
+			return fmt.Errorf("error creating config folder: %v", err)
+		}
+
+	}
+
+	// Check if sys.uuid exists, if not generate a unique UUID and write it to sys.uuid
+	uuidFilePath := configFolderPath + "/sys.uuid"
+	if _, err := os.Stat(uuidFilePath); os.IsNotExist(err) {
+		newUUID := uuid.New().String()
+		if err := os.WriteFile(uuidFilePath, []byte(newUUID), 0644); err != nil {
+			return fmt.Errorf("error writing UUID to file: %v", err)
+		}
+	}
+
+	// Read the UUID from sys.uuid
+	uuidBytes, err := os.ReadFile(uuidFilePath)
+	if err != nil {
+		return fmt.Errorf("error reading UUID from file: %v", err)
+	}
+	sysuuid = string(uuidBytes)
+
+	/* File system handler */
+	if *devMode {
+		fmt.Println("Development mode enabled. Serving files from ./web directory.")
+		webfs = http.Dir("./web")
+	} else {
+		fmt.Println("Production mode enabled. Serving files from embedded filesystem.")
+		subFS, err := fs.Sub(embeddedFiles, "web")
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error accessing embedded subdirectory: %v\n", err)
+			os.Exit(1)
+		}
+		webfs = http.FS(subFS)
+	}
+
+	/* Network statistics */
+	nsb, err := netstat.NewNetStatBuffer(300)
+	if err != nil {
+		return fmt.Errorf("error creating netstat buffer: %v", err)
+	}
+	netstatBuffer = nsb
+
+	/* Package Check */
+	if !checkRuntimeEnvironment() {
+		return fmt.Errorf("runtime environment check failed")
+	}
+
+	/* RAID Manager */
+	rm, err := raid.NewRaidManager()
+	if err != nil {
+		return err
+	}
+	raidManager = rm
+
+	/* CSRF Middleware */
+	csrfMiddleware = csrf.Protect(
+		[]byte(sysuuid),
+		csrf.CookieName(CSRF_COOKIENAME),
+		csrf.Secure(false),
+		csrf.Path("/"),
+		csrf.SameSite(csrf.SameSiteLaxMode),
+	)
+
+	return nil
+}
+
+// Cleanup function to be called on exit
+func cleanup() {
+	fmt.Println("Performing cleanup tasks...")
+	// Close the netstat buffer if it was initialized
+	if netstatBuffer != nil {
+		fmt.Println("Closing netstat buffer...")
+		netstatBuffer.Close()
+	}
+
+	fmt.Println("Cleanup completed.")
+}

+ 19 - 16
web/components/disks.html

@@ -1,20 +1,23 @@
-
-<div id="disk-list">
-    <div class="ts-box ts-content disk-info">
-        <span class="ts-icon is-spinning is-circle-notch-icon"></span>
-        <span class="has-start-padded-small" i18n>
-            Loading...
-        </span>
+<div class="ts-content">
+    <div class="ts-container is-padded">
+        <div id="disk-list">
+            <div class="ts-box ts-content disk-info">
+                <span class="ts-icon is-spinning is-circle-notch-icon"></span>
+                <span class="has-start-padded-small" i18n>
+                    Loading...
+                </span>
+            </div>
+        </div>
+        <div class="ts-wrap is-end-aligned">
+            <button id="refresh_disk_list_btn" class="ts-button is-start-icon has-top-spaced-large" >
+                <span class="ts-icon is-rotate-icon"></span>
+                <span i18n>
+                    Refresh
+                    // 重新整理
+            </button>
+        </div>
     </div>
-</div>
-<div class="ts-wrap is-end-aligned">
-    <button id="refresh_disk_list_btn" class="ts-button is-start-icon has-top-spaced-large" >
-        <span class="ts-icon is-rotate-icon"></span>
-        <span i18n>
-            Refresh
-            // 重新整理
-    </button>
-</div>
+</div>  
 <script>
     function humanFileSize(size) {
         var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));

+ 254 - 0
web/components/raid.html

@@ -0,0 +1,254 @@
+<div class="ts-content">
+    <div class="ts-container is-padded">
+        <div class="ts-grid mobile:is-stacked">
+            <div class="column is-6-wide">
+                <div id="raid_array_list" class="ts-menu is-start-icon is-separated">
+                    <a class="item">
+                        <span class="ts-icon is-user-icon"></span> 使用者
+                    </a>
+                    <a class="item is-active">
+                        <span class="ts-icon is-house-icon"></span> 首頁
+                    </a>
+                    <a class="item">
+                        <span class="ts-icon is-newspaper-icon"></span> 新聞
+                    </a>
+                </div>
+                <div class="ts-divider has-top-spaced-small"></div>
+                <div class="ts-content is-center-aligned">
+                    <button class="ts-button is-start-icon is-positive is-circular">
+                        <span class="ts-icon is-circle-plus-icon" style="color: var(--ts-positive-500);"></span>
+                        <span i18n>Create RAID
+                        // 新增陣列
+                        </span>
+                    </button>
+                    <button class="ts-button is-start-icon is-positive is-circular">
+                        <span class="ts-icon is-rotate-icon" style="color: var(--ts-primary-500);"></span>
+                        <span i18n>Assemble
+                        // 重組陣列
+                        </span>
+                    </button>
+                </div>
+            </div>
+            <div class="column is-fluid">
+                <div id="raid_details">
+                    
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    
+
+    function initRAIDDeviceList(){
+        // Utility function to convert bytes to human-readable format
+        function bytesToHumanReadable(bytes) {
+            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+            if (bytes === 0) return '0 B';
+            const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
+            return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
+        }
+        $.ajax({
+            url: './api/raid/list',
+            type: 'GET',
+            dataType: 'json',
+            success: function(data) {
+                $('#raid_array_list').html("");
+                if (data.error != undefined){
+                    // Handle error response
+                    console.error('Error fetching RAID devices:', data.error);
+                    $('#raid_array_list').append('<div class="ts-text is-error">Error: ' + data.error + '</div>');
+                }else{
+                    let isSyncing = false;
+                    data.forEach((raid, index) => {
+                        // Add a new menu item for each RAID array
+                        let icon = '';
+                        if (raid.State.includes('clean') && !raid.State.includes('sync')) {
+                            icon = '<span class="ts-icon is-check-icon" style="color: var(--ts-positive-500);"></span>';
+                        } else if (raid.State.includes('sync')) {
+                            isSyncing = true;
+                            icon = '<span class="ts-icon is-spinning is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
+                        } else if (raid.State.includes('degraded')) {
+                            icon = '<span class="ts-icon is-triangle-exclamation-icon" style="color: var(--ts-warning-600);"></span>';
+                        } else if (raid.State.includes('fail')) {
+                            icon = '<span class="ts-icon is-circle-xmark-icon" style="color: var(--ts-negative-500);"></span>';
+                        } else {
+                            icon = '<span class="ts-icon is-question-icon" style="color: var(--ts-gray-500);"></span>';
+                        }
+                        const menuItem = `
+                            <a class="raid-array item ${index==0?'is-active':''}" id="raid_menu_${index}" onclick="showRAIDDetails(${index})">
+                                ${icon}
+                                <div class="ts-content is-dense">
+                                    <div>
+                                        <span class="ts-text is-heavy">${raid.DevicePath}</span> | ${raid.RaidLevel.toUpperCase()}
+                                    </div>
+                                    <div class="ts-text is-tiny has-top-spaced-small">
+                                        ${raid.Name}
+                                    </div>
+                                </div>
+                            </a>
+                        `;
+                        $('#raid_array_list').append(menuItem);
+                        
+                        // Add a hidden div for each RAID array's details
+                        const raidDetails = `
+                            <div id="raid_details_${index}" class="raid-details" style="display: none ;">
+                                <div class="ts-box">
+                                    <div class="ts-content is-padded">
+                                        <div class="ts-header is-start-icon">
+                                            ${icon}
+                                            ${raid.DevicePath} | ${raid.RaidLevel.toUpperCase()}
+                                        </div>
+                                        <div class="ts-text is-description">
+                                            ${raid.UUID}<br>
+                                            ${raid.Name}
+                                        </div>
+                                        <div class="ts-text">
+                                            <span i18n> State
+                                                // 狀態
+                                            </span>: ${raid.State}<br>
+                                            <div class="sync-progress has-top-spaced-small ${isSyncing?'need-update-raid-sync-progress':''}" devname="${raid.DevicePath}" style="display: ${isSyncing?"auto":"none"};">
+                                                <div class="ts-progress is-processing">
+                                                <div class="bar" style="--value: 0">
+                                                        <div class="text">0%</div>
+                                                    </div>
+                                                </div>
+                                                <div class="ts-text is-description has-top-spaced-small">
+                                                    <span i18n> Synchronized
+                                                        // 已處理</span>
+                                                    <span class="processed_blocks"></span>
+                                                    <span>/</span>
+                                                    <span class="total_blocks"></span>
+                                                    <span i18n> blocks
+                                                        // 個區塊
+                                                    </span><br>
+                                                    <!-- <span i18n> Speed
+                                                        // 速度
+                                                    </span>: <span class="speed"></span><br>
+                                                    <span i18n> Expected Time
+                                                        // 預估時間
+                                                    </span>: <span class="expected_time"></span>
+                                                    -->
+                                                </div>
+                                            </div>
+                                            <span i18n> Array Size
+                                                // 陣列大小
+                                            </span>: ${bytesToHumanReadable(raid.ArraySize * 1024)}<br>
+                                            <span i18n> Created
+                                                // 建立時間
+                                            </span>: <span>${new Date(raid.CreationTime).toLocaleString()}</span><br>
+                                        </div>
+                                        <table class="ts-table is-single-line has-top-spaced-large">
+                                            <thead>
+                                                <tr>
+                                                    <th i18n>Disk Status
+                                                        // 磁碟狀態
+                                                    </th>
+                                                    <th i18n>Counts 
+                                                        // 數量
+                                                    </th>
+                                                </tr>
+                                            </thead>
+                                            <tbody>
+                                                <tr>
+                                                    <td i18n> Active Devices
+                                                        // 啟用的磁碟
+                                                    </td>
+                                                    <td>${raid.ActiveDevices}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td i18n> Working Devices
+                                                        // 工作中的磁碟
+                                                    </td>
+                                                    <td>${raid.WorkingDevices}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td i18n> Failed Devices
+                                                        // 故障的磁碟
+                                                    </td>
+                                                    <td>${raid.FailedDevices}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td i18n> Spare Devices
+                                                        // 備用磁碟
+                                                    </td>
+                                                    <td>${raid.SpareDevices}</td>
+                                                </tr>
+                                            </tbody>
+                                        </table>
+                                    </div>
+                                </div>
+                                <div class="ts-box is-padded has-top-spaced-small">
+                                    <div class="ts-content">
+                                        
+                                    </div>
+                                </div>
+                            </div>
+                        `;
+                        $('#raid_details').append(raidDetails);
+                    });
+                }
+
+                relocale(); // Recalculate layout
+                syncProgressTicker(); // Start the sync progress ticker
+            },
+            error: function(xhr, status, error) {
+                console.error('Error fetching RAID devices:', error);
+            }
+        });
+    }
+
+    initRAIDDeviceList();
+
+    //Create a ticker to check for RAID sync progress
+    function syncProgressTicker(){
+        let syncProgressTracker = $(".need-update-raid-sync-progress");
+        if (syncProgressTracker.length > 0){
+            syncProgressTracker.each(function(){
+                let devname = $(this).attr("devname");
+                $.ajax({
+                    url: './api/raid/sync?dev=' + devname,
+                    type: 'GET',
+                    dataType: 'json',
+                    data: { devname: devname },
+                    success: function(data) {
+                        if (data.error != undefined){
+                            // Handle error response
+                            console.error('Error fetching RAID sync progress:', data.error);
+                            //Refresh the RAID list
+                            initRAIDDeviceList();
+                        }else{
+                            let progress = parseFloat(data.ResyncPercent);
+                            let total_blocks = parseInt(data.TotalBlocks);
+                            let processed_blocks = parseInt(data.CompletedBlocks);
+                            let expected_time = data.ExpectedTime;
+                            let speed = data.Speed;
+
+                            $(`.sync-progress[devname="${devname}"] .bar`).css('--value', progress);
+                            $(`.sync-progress[devname="${devname}"] .bar .text`).text(`${progress.toFixed(1)}%`);
+                            $(`.sync-progress[devname="${devname}"] .processed_blocks`).text(processed_blocks);
+                            $(`.sync-progress[devname="${devname}"] .total_blocks`).text(total_blocks);
+                            //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .speed`).text(speed);
+                            //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .expected_time`).text(expected_time);
+                            
+                        }
+                    },
+                    error: function(xhr, status, error) {
+                        console.error('Error fetching RAID sync progress:', error);
+                    }
+                });
+            });
+        }
+    }
+
+    setInterval(syncProgressTicker, 5000); // Check every 5 seconds
+
+    function showRAIDDetails(index) {
+        $('.raid-details').hide(); // Hide all RAID details
+        $(`#raid_details_${index}`).show(); // Show the selected RAID details
+        $('.raid-array.is-active').removeClass('is-active'); // Remove active class from all menu items
+        $(`#raid_menu_${index}`).addClass('is-active'); // Add active class to the selected menu item
+        relocale(); // Recalculate layout
+    }
+</script>
+

+ 116 - 113
web/components/status.html

@@ -1,127 +1,130 @@
-<div class="ts-content is-rounded is-padded has-top-spaced-large" style="background: var(--ts-gray-800); color: var(--ts-gray-50)">
-    <div style="max-width: 480px">
-        <div class="ts-header is-huge is-heavy">
-            <div class="sysstatus_good">
-                <span class="ts-icon is-positive is-heading is-check-icon" style="color: var(--ts-positive-500);"></span>
-                <span i18n>Looks Good
-                    // 看起來不錯
-                </span>
-            </div>
-            <div class="sysstatus_attention" style="display:none;">
-                <span class="ts-icon is-warning is-heading is-exclamation-icon" style="color: var(--ts-warning-600);"></span>
-                <span i18n>Attention Required
-                    // 需要注意
-                </span>
-            </div>
-            <div class="sysstatus_bad" style="display:none;">
-                <span class="ts-icon is-negative is-heading is-xmark-icon" style="color: var(--ts-negative-500);"></span>
-                <span i18n>Critical Error
-                    // 嚴重錯誤
-                </span>
-            </div>
-            
-        </div>
-        <p class="sysstatus_good" i18n>This status shows you the general idea on how this storage node is doing in terms of disk health and other system conditions. See system analytic report for more details.
-            // 此狀態顯示了這個儲存節點在磁碟健康和其他系統條件方面的整體情況。 有關詳細資訊,請參閱系統分析報告。
-        </p>
-        <p class="sysstatus_attention" style="display:none;" i18n>Some disks are failing soon. Check the SMART status of the disks for more details.
-            // 某些磁碟的壽命即將結束。 請檢查磁碟的 SMART 狀態。
-        </p>
-        <p class="sysstatus_bad" style="display:none;" i18n>All disks are not healthy or failed. Replace the disks as soon as possible.
-            // 所有磁碟都不健康或已損壞。 請儘快更換磁碟。
-        </p>
-        <a href="#!" class="ts-button is-outlined" style="color: var(--ts-gray-50)" i18n>
-            Analytics Report
-            // 分析報告
-        </a>
-    </div>
-</div>
-<!-- Disk SMARTs -->
-<div class="has-top-spaced-large is-padded">
-    <div id="disk-smart-overview" class="ts-grid has-top-spaced-large is-relaxed is-3-columns is-stretched mobile:is-stacked">
-        <div class="column">
-            <div class="ts-content is-rounded is-padded">
-                <div class="ts-header is-truncated is-large is-heavy" i18n>
-                    SMART Status
-                    // 磁碟健康狀態
+<div class="ts-content">
+    <div class="ts-container is-padded">
+        <div class="ts-content is-rounded is-padded has-top-spaced-large" style="background: var(--ts-gray-800); color: var(--ts-gray-50)">
+            <div style="max-width: 480px">
+                <div class="ts-header is-huge is-heavy">
+                    <div class="sysstatus_good">
+                        <span class="ts-icon is-positive is-heading is-check-icon" style="color: var(--ts-positive-500);"></span>
+                        <span i18n>Looks Good
+                            // 看起來不錯
+                        </span>
+                    </div>
+                    <div class="sysstatus_attention" style="display:none;">
+                        <span class="ts-icon is-warning is-heading is-exclamation-icon" style="color: var(--ts-warning-600);"></span>
+                        <span i18n>Attention Required
+                            // 需要注意
+                        </span>
+                    </div>
+                    <div class="sysstatus_bad" style="display:none;">
+                        <span class="ts-icon is-negative is-heading is-xmark-icon" style="color: var(--ts-negative-500);"></span>
+                        <span i18n>Critical Error
+                            // 嚴重錯誤
+                        </span>
+                    </div>
+                    
                 </div>
-                <p>
-                    <span class="ts-icon is-spinning is-circle-notch-icon"></span>
-                    <span i18n>Loading
-                        // 載入中
-                    </span>
+                <p class="sysstatus_good" i18n>This status shows you the general idea on how this storage node is doing in terms of disk health and other system conditions. See system analytic report for more details.
+                    // 此狀態顯示了這個儲存節點在磁碟健康和其他系統條件方面的整體情況。 有關詳細資訊,請參閱系統分析報告。
+                </p>
+                <p class="sysstatus_attention" style="display:none;" i18n>Some disks are failing soon. Check the SMART status of the disks for more details.
+                    // 某些磁碟的壽命即將結束。 請檢查磁碟的 SMART 狀態。
                 </p>
+                <p class="sysstatus_bad" style="display:none;" i18n>All disks are not healthy or failed. Replace the disks as soon as possible.
+                    // 所有磁碟都不健康或已損壞。 請儘快更換磁碟。
+                </p>
+                <a href="#!" class="ts-button is-outlined" style="color: var(--ts-gray-50)" i18n>
+                    Analytics Report
+                    // 分析報告
+                </a>
             </div>
         </div>
-    </div>
-</div>  
+        <!-- Disk SMARTs -->
+        <div class="has-top-spaced-large is-padded">
+            <div id="disk-smart-overview" class="ts-grid has-top-spaced-large is-relaxed is-3-columns is-stretched mobile:is-stacked">
+                <div class="column">
+                    <div class="ts-content is-rounded is-padded">
+                        <div class="ts-header is-truncated is-large is-heavy" i18n>
+                            SMART Status
+                            // 磁碟健康狀態
+                        </div>
+                        <p>
+                            <span class="ts-icon is-spinning is-circle-notch-icon"></span>
+                            <span i18n>Loading
+                                // 載入中
+                            </span>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>  
 
-<!-- Network IO -->
-<div class="ts-box has-top-spaced-large is-rounded is-padded ">
-    <div class="ts-content">
-        <div class="ts-header" i18n>Real-time Network IO
-            // 即時網路流量
-        </div>
-        <div id="networkActWrapper" class="has-top-spaced-large" style="position: relative;">
-            <canvas id="networkActivity"></canvas>
-        </div>
-        <div id="networkActivityPlaceHolder" class="ts-blankslate is-secondary" style="display:none;"> 
-            <div class="header" i18n>Graph Render Paused 
-                // 已暫停圖表運算
+        <!-- Network IO -->
+        <div class="ts-box has-top-spaced-large is-rounded is-padded ">
+            <div class="ts-content">
+                <div class="ts-header" i18n>Real-time Network IO
+                    // 即時網路流量
+                </div>
+                <div id="networkActWrapper" class="has-top-spaced-large" style="position: relative;">
+                    <canvas id="networkActivity"></canvas>
+                </div>
+                <div id="networkActivityPlaceHolder" class="ts-blankslate is-secondary" style="display:none;"> 
+                    <div class="header" i18n>Graph Render Paused 
+                        // 已暫停圖表運算
+                    </div>
+                    <div class="description" i18n>Graph resumes after resizing or refocus
+                        // 當頁面調整大小或重新聚焦後,圖表將恢復運算
+                    </div>
+                </div>
             </div>
-            <div class="description" i18n>Graph resumes after resizing or refocus
-                // 當頁面調整大小或重新聚焦後,圖表將恢復運算
+            <div class="ts-content is-dense">
+                <i class="ts-icon is-end-spaced is-circle-down-icon" style="color: #1890ff;"></i>
+                <span i18n>Inbound Traffic
+                    // 進站流量
+                </span>
+                <i class="ts-icon is-end-spaced has-start-spaced-large is-circle-up-icon" style="color: #52c41a;"></i>
+                <span i18n>Outbound Traffic
+                    // 出站流量
+                </span>
+            
             </div>
-        </div>
-    </div>
-    <div class="ts-content is-dense">
-        <i class="ts-icon is-end-spaced is-circle-down-icon" style="color: #1890ff;"></i>
-        <span i18n>Inbound Traffic
-            // 進站流量
-        </span>
-        <i class="ts-icon is-end-spaced has-start-spaced-large is-circle-up-icon" style="color: #52c41a;"></i>
-        <span i18n>Outbound Traffic
-            // 出站流量
-        </span>
-    
-    </div>
-</div>  
+        </div>  
 
-<!-- Network Interface -->
-<div class="has-top-spaced-large is-padded">
-    <div class="ts-content">
-        <div class="ts-header is-truncated is-large is-heavy" i18n>Network Interfaces
-            // 網路介面
+        <!-- Network Interface -->
+        <div class="has-top-spaced-large is-padded">
+            <div class="ts-content">
+                <div class="ts-header is-truncated is-large is-heavy" i18n>Network Interfaces
+                    // 網路介面
+                </div>
+                <p i18n>List of network interfaces and their IP addresses.
+                    // 網路介面及其 IP 位址列表。
+                </p>
+            </div>
+            <table class="ts-table is-striped">
+                <thead>
+                    <tr>
+                        <th>ID</th>
+                        <th i18n>iface name
+                            // 介面名稱
+                        </th>
+                        <th i18n>IP Address
+                            // IP 位址
+                        </th>
+                    </tr>
+                </thead>
+                <tbody id="network-interface-list">
+                    <tr>
+                        <td colspan="3">
+                            <span class="ts-icon is-spinning is-circle-notch-icon"></span>
+                            <span i18n>Loading
+                                // 載入中
+                            </span>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
         </div>
-        <p i18n>List of network interfaces and their IP addresses.
-            // 網路介面及其 IP 位址列表。
-        </p>
     </div>
-    <table class="ts-table is-striped">
-        <thead>
-            <tr>
-                <th>ID</th>
-                <th i18n>iface name
-                    // 介面名稱
-                </th>
-                <th i18n>IP Address
-                    // IP 位址
-                </th>
-            </tr>
-        </thead>
-        <tbody id="network-interface-list">
-            <tr>
-                <td colspan="3">
-                    <span class="ts-icon is-spinning is-circle-notch-icon"></span>
-                    <span i18n>Loading
-                        // 載入中
-                    </span>
-                </td>
-            </tr>
-        </tbody>
-    </table>
 </div>
-
 <script>
 
     /* Network Interface */

+ 15 - 0
web/img/raid/drive-add.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.729-1.357,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.397-3.035-3.127V86.002
+	c0-1.729,1.359-3.127,3.035-3.127h92.556c1.677,0,3.034,1.398,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+<circle fill="#33B418" cx="102.083" cy="51.5" r="20"/>
+<g>
+	<path fill="#FFFFFF" d="M112.599,53.259h-8.796v8.756h-3.458v-8.756h-8.776v-3.498h8.776v-8.776h3.458v8.776h8.796V53.259z"/>
+</g>
+</svg>

+ 14 - 0
web/img/raid/drive-failed.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.729-1.357,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.397-3.035-3.127V86.002
+	c0-1.729,1.359-3.127,3.035-3.127h92.556c1.677,0,3.034,1.398,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+<circle fill="#C30D23" cx="101.73" cy="48.736" r="20.897"/>
+<line fill="none" stroke="#FFFFFF" stroke-width="5" stroke-miterlimit="10" x1="110.299" y1="40.167" x2="93.162" y2="57.305"/>
+<line fill="none" stroke="#FFFFFF" stroke-width="5" stroke-miterlimit="10" x1="110.299" y1="57.305" x2="93.162" y2="40.167"/>
+</svg>

+ 14 - 0
web/img/raid/drive-format.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.729-1.357,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.396-3.035-3.127V86.002
+	c0-1.729,1.359-3.127,3.035-3.127h92.556c1.677,0,3.034,1.398,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+<path fill="#FFFFFF" d="M84.535,64.426c-5.24,5.244-14.758,4.226-21.258-2.275c-6.499-6.498-7.52-16.018-2.276-21.26
+	c5.242-5.24,14.761-4.221,21.258,2.276C88.76,49.667,89.781,59.186,84.535,64.426z"/>
+<rect x="66.415" y="19.963" transform="matrix(0.7071 -0.7071 0.7071 0.7071 0.1325 73.5299)" fill="#00A0E9" width="44.814" height="33.285"/>
+</svg>

+ 35 - 0
web/img/raid/drive-notfound.svg

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
+	 height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<g id="圖層_1">
+	<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+	<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 	"/>
+	<path fill="#3E3A39" d="M113.125,105.811c0,1.728-1.358,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.399-3.035-3.127V86.002
+		c0-1.728,1.359-3.127,3.035-3.127h92.556c1.676,0,3.034,1.399,3.034,3.127V105.811z"/>
+	<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+</g>
+<g id="圖層_2">
+</g>
+<g id="圖層_3">
+	<circle fill="#3E3A39" cx="94.833" cy="36" r="25.5"/>
+	<g>
+		<path fill="#F7F8F8" d="M91.648,40.194v-1.445c0-0.664,0.068-1.266,0.205-1.807c0.137-0.54,0.346-1.055,0.625-1.543
+			c0.28-0.488,0.642-0.963,1.084-1.426c0.443-0.462,0.977-0.94,1.602-1.436c0.547-0.43,1.01-0.813,1.387-1.152
+			c0.378-0.338,0.684-0.674,0.918-1.006s0.404-0.68,0.508-1.045c0.104-0.364,0.156-0.788,0.156-1.27c0-0.742-0.25-1.354-0.752-1.836
+			c-0.501-0.481-1.266-0.723-2.295-0.723c-0.898,0-1.865,0.189-2.9,0.566c-1.035,0.378-2.105,0.84-3.213,1.387l-1.992-4.316
+			c0.561-0.325,1.169-0.635,1.826-0.928c0.658-0.293,1.342-0.553,2.051-0.781c0.71-0.228,1.43-0.407,2.158-0.537
+			c0.729-0.13,1.445-0.195,2.148-0.195c1.328,0,2.526,0.16,3.594,0.479c1.068,0.319,1.973,0.785,2.715,1.396
+			c0.742,0.612,1.313,1.354,1.709,2.227c0.398,0.873,0.596,1.869,0.596,2.988c0,0.82-0.09,1.553-0.273,2.197
+			c-0.182,0.645-0.452,1.244-0.811,1.797c-0.357,0.554-0.807,1.087-1.348,1.602c-0.54,0.515-1.168,1.058-1.885,1.631
+			c-0.547,0.43-0.992,0.804-1.338,1.123c-0.345,0.319-0.615,0.622-0.811,0.908c-0.195,0.287-0.328,0.583-0.4,0.889
+			c-0.071,0.306-0.107,0.667-0.107,1.084v1.172H91.648z M91.004,46.874c0-0.612,0.088-1.129,0.264-1.553
+			c0.176-0.423,0.42-0.765,0.732-1.025c0.313-0.26,0.681-0.449,1.104-0.566c0.424-0.117,0.876-0.176,1.357-0.176
+			c0.456,0,0.889,0.059,1.299,0.176s0.771,0.306,1.084,0.566c0.313,0.261,0.561,0.603,0.742,1.025
+			c0.183,0.423,0.273,0.941,0.273,1.553c0,0.586-0.091,1.087-0.273,1.504c-0.182,0.417-0.43,0.762-0.742,1.035
+			s-0.674,0.472-1.084,0.596c-0.41,0.124-0.843,0.186-1.299,0.186c-0.481,0-0.934-0.062-1.357-0.186
+			c-0.423-0.124-0.791-0.322-1.104-0.596s-0.557-0.618-0.732-1.035C91.092,47.961,91.004,47.459,91.004,46.874z"/>
+	</g>
+</g>
+</svg>

+ 14 - 0
web/img/raid/drive-spare.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.729-1.357,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.396-3.035-3.127V86.002
+	c0-1.729,1.359-3.127,3.035-3.127h92.556c1.677,0,3.034,1.398,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+<circle fill="#908FB3" cx="101.652" cy="48.736" r="20.897"/>
+<polygon fill="#FFFFFF" points="92.373,59.866 101.733,46.104 110.932,60.068 "/>
+<polygon fill="#FFFFFF" points="111.088,37.605 101.728,51.368 92.529,37.403 "/>
+</svg>

+ 14 - 0
web/img/raid/drive-working.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.729-1.357,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.398-3.035-3.127V86.002
+	c0-1.729,1.359-3.127,3.035-3.127h92.556c1.677,0,3.034,1.398,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+<circle fill="#33B418" cx="102.083" cy="51.5" r="20"/>
+<polyline fill="none" stroke="#FFFFFF" stroke-width="5" stroke-miterlimit="10" points="111.234,46.214 99.795,56.786 
+	92.932,50.868 "/>
+</svg>

+ 11 - 0
web/img/raid/drive.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 "/>
+<path fill="#3E3A39" d="M113.125,105.811c0,1.728-1.358,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.399-3.035-3.127V86.002
+	c0-1.728,1.359-3.127,3.035-3.127h92.556c1.676,0,3.034,1.399,3.034,3.127V105.811z"/>
+<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+</svg>

+ 21 - 0
web/img/raid/raid.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
+	 height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<g id="圖層_1">
+	<rect x="12.563" y="32.823" fill="#727171" width="102.834" height="86.829"/>
+	<path fill="#3E3A39" d="M113.23,114.962c0,1.729-1.357,3.127-3.035,3.127H17.64c-1.676,0-3.035-1.398-3.035-3.127V95.153
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V114.962z"/>
+	<circle fill="#00A0E9" cx="100.48" cy="105.066" r="3.708"/>
+</g>
+<g id="圖層_2">
+	<polygon fill="#DCDDDD" points="96.438,14.002 30.271,14.002 12.604,32.823 115.438,32.823 	"/>
+	<path fill="#3E3A39" d="M113.023,85.822c0,1.729-1.357,3.127-3.035,3.127H17.433c-1.676,0-3.035-1.398-3.035-3.127V66.014
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V85.822z"/>
+	<circle fill="#00A0E9" cx="100.273" cy="75.929" r="3.708"/>
+	<path fill="#3E3A39" d="M112.92,57.3c0,1.729-1.357,3.127-3.035,3.127H17.329c-1.676,0-3.035-1.398-3.035-3.127V37.492
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V57.3z"/>
+	<circle fill="#00A0E9" cx="100.17" cy="47.406" r="3.708"/>
+</g>
+</svg>

+ 28 - 0
web/img/raid/remove-warning.svg

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<g id="圖層_1_1_">
+	<rect x="12.563" y="32.823" fill="#727171" width="102.833" height="86.83"/>
+	<path fill="#3E3A39" d="M113.23,114.962c0,1.729-1.357,3.127-3.035,3.127H17.64c-1.676,0-3.035-1.397-3.035-3.127v-19.81
+		c0-1.729,1.359-3.127,3.035-3.127h92.555c1.679,0,3.035,1.398,3.035,3.127V114.962L113.23,114.962z"/>
+	<circle fill="#00A0E9" cx="100.48" cy="105.065" r="3.708"/>
+</g>
+<g id="圖層_2">
+	<polygon fill="#DCDDDD" points="96.438,14.002 30.271,14.002 12.604,32.823 115.438,32.823 	"/>
+	<path fill="#3E3A39" d="M113.023,85.822c0,1.729-1.357,3.127-3.035,3.127H17.433c-1.676,0-3.035-1.398-3.035-3.127V66.014
+		c0-1.729,1.359-3.126,3.035-3.126h92.555c1.679,0,3.035,1.398,3.035,3.126V85.822L113.023,85.822z"/>
+	<circle fill="#00A0E9" cx="100.273" cy="75.929" r="3.708"/>
+	<path fill="#3E3A39" d="M112.92,57.3c0,1.729-1.357,3.127-3.035,3.127H17.329c-1.676,0-3.035-1.398-3.035-3.127V37.492
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V57.3z"/>
+	<circle fill="#00A0E9" cx="100.17" cy="47.406" r="3.708"/>
+</g>
+<circle fill="#FF1D1D" cx="99.582" cy="98.738" r="23.667"/>
+<g>
+	<path fill="#FFFFFF" d="M99.582,116.816c-0.956,0-1.774-0.337-2.456-1.011c-0.682-0.673-1.022-1.512-1.022-2.515
+		c0-0.971,0.333-1.786,0.999-2.444s1.492-0.986,2.479-0.986s1.813,0.328,2.479,0.986s0.999,1.474,0.999,2.444
+		c0,1.003-0.333,1.842-0.999,2.515C101.396,116.479,100.569,116.816,99.582,116.816z M97.89,104.783l-0.563-22.984h4.418
+		l-0.563,22.984H97.89z"/>
+</g>
+</svg>

+ 13 - 12
web/index.html

@@ -27,6 +27,8 @@
             z-index: 9999;
         }
     </style>
+
+    <link rel="icon" type="image/png" href="img/favicon.png">
 </head>
 <body>
     <div class="ts-content ">
@@ -48,7 +50,7 @@
                 <a href="" class="item" style="user-select: none;">
                     <img id="sysicon" class="ts-image" style="height: 30px" src="img/logo.svg"></img>
                 </a>
-                <button href="#!" class="item is-active" data-tab="tab-status" i18n>
+                <button href="#!" class="item" data-tab="tab-status" i18n>
                     Status
                     // 系統狀態
                 </button>
@@ -60,7 +62,7 @@
                     Disks
                     // 磁碟資訊
                 </button>
-                <button href="#!" class="item" data-tab="tab-raid" i18n>
+                <button href="#!" class="item is-active" data-tab="tab-raid" i18n>
                     RAID
                     // 磁碟陣列
                 </button>
@@ -80,16 +82,14 @@
         </div>
     </div>
     <div class="ts-divider"></div>
-    <div class="ts-content is-vertically-padded">
-        <div class="ts-container">
-            <div class="ts-content boko-panel-component" id="tab-status" component="status.html">Status</div>
-            <div class="ts-content boko-panel-component" id="tab-connections" component="connections.html">Connections</div>
-            <div class="ts-content boko-panel-component" id="tab-disks" component="disks.html">Disks</div>
-            <div class="ts-content boko-panel-component" id="tab-raid" component="raid.html">RAID</div>
-            <div class="ts-content boko-panel-component" id="tab-tools">Tools</div>
-            <div class="ts-content boko-panel-component" id="tab-logs">Logs</div>
-            <div class="ts-content boko-panel-component" id="tab-settings">Settings</div>
-        </div>  
+    <div>
+        <div class="boko-panel-component" id="tab-status" component="status.html">Status</div>
+        <div class="boko-panel-component" id="tab-connections" component="connections.html">Connections</div>
+        <div class="boko-panel-component" id="tab-disks" component="disks.html">Disks</div>
+        <div class="boko-panel-component" id="tab-raid" component="raid.html">RAID</div>
+        <div class="boko-panel-component" id="tab-tools">Tools</div>
+        <div class="boko-panel-component" id="tab-logs">Logs</div>
+        <div class="boko-panel-component" id="tab-settings">Settings</div>
     </div>
     <div class="ts-container">
         <div class="ts-divider"></div>
@@ -132,6 +132,7 @@
             $("#msgbox").stop().finish().fadeOut(200);
         });
         $("#msgbox").hide();
+    
     </script>
     <script src="./js/locale.js"></script>
 </body>