TC 2 недель назад
Родитель
Сommit
1f2f84049f
10 измененных файлов с 436 добавлено и 176 удалено
  1. 1 1
      main.go
  2. 22 22
      mod/diskinfo/typedef.go
  3. 26 1
      mod/disktool/raid/handler.go
  4. 29 0
      mod/disktool/raid/sync.go
  5. 7 12
      raid.go
  6. 64 0
      start.go
  7. 7 0
      web/components/disks.html
  8. 263 140
      web/components/raid.html
  9. 15 0
      web/index.html
  10. 2 0
      web/js/locale.js

+ 1 - 1
main.go

@@ -64,7 +64,7 @@ func main() {
 	//END DEBUG
 
 	/* Static Web Server */
-	http.Handle("/", csrfMiddleware(http.FileServer(webfs)))
+	http.Handle("/", csrfMiddleware(tmplMiddleware(http.FileServer(webfs))))
 
 	/* WebDAV Handlers */
 	http.Handle("/disk/", wds.FsHandler())     //Note the trailing slash

+ 22 - 22
mod/diskinfo/typedef.go

@@ -2,31 +2,31 @@ package diskinfo
 
 // Disk represents a disk device with its attributes.
 type Disk struct {
-	Name       string       `json:"name"`                 // Name of the disk, e.g. sda
-	Identifier string       `json:"identifier"`           // Disk identifier, e.g. 0x12345678
-	Model      string       `json:"model"`                // Disk model, e.g. Samsung SSD 860 EVO 1TB
-	Size       int64        `json:"size"`                 // Size of the disk in bytes
-	Used       int64        `json:"used"`                 // Used space in bytes, calculated from partitions
-	Free       int64        `json:"free"`                 // Free space in bytes, calculated as Size - Used
-	DiskLabel  string       `json:"disklabel"`            // Disk label type, e.g. gpt
-	BlockType  string       `json:"blocktype"`            // Type of the block device, e.g. disk
-	Partitions []*Partition `json:"partitions,omitempty"` // List of partitions on the disk
+	Name       string       `json:"name"`       // Name of the disk, e.g. sda
+	Identifier string       `json:"identifier"` // Disk identifier, e.g. 0x12345678
+	Model      string       `json:"model"`      // Disk model, e.g. Samsung SSD 860 EVO 1TB
+	Size       int64        `json:"size"`       // Size of the disk in bytes
+	Used       int64        `json:"used"`       // Used space in bytes, calculated from partitions
+	Free       int64        `json:"free"`       // Free space in bytes, calculated as Size - Used
+	DiskLabel  string       `json:"disklabel"`  // Disk label type, e.g. gpt
+	BlockType  string       `json:"blocktype"`  // Type of the block device, e.g. disk
+	Partitions []*Partition `json:"partitions"` // List of partitions on the disk
 }
 
 // Partition represents a partition on a disk with its attributes.
 type Partition struct {
-	UUID       string `json:"uuid"`                 // UUID of the file system
-	PartUUID   string `json:"partuuid"`             // Partition UUID
-	PartLabel  string `json:"partlabel"`            // Partition label
-	Name       string `json:"name"`                 // Name of the partition, e.g. sda1
-	Path       string `json:"path"`                 // Path of the partition, e.g. /dev/sda1
-	Size       int64  `json:"size"`                 // Size of the partition in bytes
-	Used       int64  `json:"used"`                 // Used space in bytes
-	Free       int64  `json:"free"`                 // Free space in bytes
-	BlockSize  int    `json:"blocksize"`            // Block size in bytes, e.g. 4096
-	BlockType  string `json:"blocktype"`            // Type of the block device, e.g. part
-	FsType     string `json:"fstype"`               // File system type, e.g. ext4
-	MountPoint string `json:"mountpoint,omitempty"` // Mount point of the partition, e.g. /mnt/data
+	UUID       string `json:"uuid"`       // UUID of the file system
+	PartUUID   string `json:"partuuid"`   // Partition UUID
+	PartLabel  string `json:"partlabel"`  // Partition label
+	Name       string `json:"name"`       // Name of the partition, e.g. sda1
+	Path       string `json:"path"`       // Path of the partition, e.g. /dev/sda1
+	Size       int64  `json:"size"`       // Size of the partition in bytes
+	Used       int64  `json:"used"`       // Used space in bytes
+	Free       int64  `json:"free"`       // Free space in bytes
+	BlockSize  int    `json:"blocksize"`  // Block size in bytes, e.g. 4096
+	BlockType  string `json:"blocktype"`  // Type of the block device, e.g. part
+	FsType     string `json:"fstype"`     // File system type, e.g. ext4
+	MountPoint string `json:"mountpoint"` // Mount point of the partition, e.g. /mnt/data
 }
 
 // Block represents a block device with its attributes.
@@ -38,5 +38,5 @@ type Block struct {
 	BlockSize  int    `json:"blocksize"`
 	BlockType  string `json:"blocktype"`
 	FsType     string `json:"fstype"`
-	MountPoint string `json:"mountpoint,omitempty"`
+	MountPoint string `json:"mountpoint"`
 }

+ 26 - 1
mod/disktool/raid/handler.go

@@ -241,7 +241,7 @@ func (m *Manager) HandleListUsableDevices(w http.ResponseWriter, r *http.Request
 
 // Handle loading the detail of a given RAID array
 func (m *Manager) HandleLoadArrayDetail(w http.ResponseWriter, r *http.Request) {
-	devName, err := utils.GetPara(r, "devName")
+	devName, err := utils.GetPara(r, "dev")
 	if err != nil {
 		utils.SendErrorResponse(w, "invalid device name given")
 		return
@@ -680,6 +680,8 @@ func (m *Manager) HandleRenderOverview(w http.ResponseWriter, r *http.Request) {
 	utils.SendJSONResponse(w, string(js))
 }
 
+/* Sync State Related Features */
+// HandleGetRAIDSyncState Get the sync state of a given RAID device
 func (m *Manager) HandleGetRAIDSyncState(w http.ResponseWriter, r *http.Request) {
 	devName, err := utils.GetPara(r, "dev")
 	if err != nil {
@@ -701,3 +703,26 @@ func (m *Manager) HandleGetRAIDSyncState(w http.ResponseWriter, r *http.Request)
 	js, _ := json.Marshal(syncState)
 	utils.SendJSONResponse(w, string(js))
 }
+
+// HandleSyncPendingToReadWrite Set the pending sync to read-write mode
+// to reactivate the resync process
+func (m *Manager) HandleSyncPendingToReadWrite(w http.ResponseWriter, r *http.Request) {
+	devName, err := utils.PostPara(r, "dev")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid device name given")
+		return
+	}
+
+	if !strings.HasPrefix(devName, "/dev/") {
+		devName = filepath.Join("/dev/", devName)
+	}
+
+	//Set the pending sync to read-write mode
+	err = m.SetSyncPendingToReadWrite(devName)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	utils.SendOK(w)
+}

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

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"os/exec"
 	"strings"
 )
 
@@ -102,3 +103,31 @@ func (m *Manager) GetSyncStates() ([]SyncState, error) {
 
 	return syncStates, nil
 }
+
+// SetSyncPendingToReadWrite sets the RAID device to read-write mode.
+// After a RAID array is created, it may be in a "sync-pending" state.
+// This function changes the state to "read-write".
+func (m *Manager) SetSyncPendingToReadWrite(devname string) error {
+	// Ensure devname does not already have the /dev/ prefix
+	devname = strings.TrimPrefix(devname, "/dev/")
+
+	// Check if the current user has root privileges by checking the UID
+	hasSudo := os.Geteuid() == 0
+
+	//Check if the device exists
+	if _, err := os.Stat(fmt.Sprintf("/dev/%s", devname)); os.IsNotExist(err) {
+		return fmt.Errorf("device %s does not exist", devname)
+	}
+
+	// Construct the command
+	var cmd *exec.Cmd
+	if hasSudo {
+		cmd = exec.Command("sudo", "mdadm", "--readwrite", fmt.Sprintf("/dev/%s", devname))
+	} else {
+		cmd = exec.Command("mdadm", "--readwrite", fmt.Sprintf("/dev/%s", devname))
+	}
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to set device %s to readwrite: %v, output: %s", devname, err, string(output))
+	}
+	return nil
+}

+ 7 - 12
raid.go

@@ -1,11 +1,8 @@
 package main
 
 import (
-	"encoding/json"
 	"net/http"
 	"strings"
-
-	"imuslab.com/bokofs/bokofsd/mod/utils"
 )
 
 /*
@@ -23,19 +20,17 @@ func HandleRAIDCalls() http.Handler {
 			// List all RAID devices
 			raidManager.HandleListRaidDevices(w, r)
 			return
+		case "info":
+			// Handle loading the detail of a given RAID array, require "dev=md0" as a query parameter
+			raidManager.HandleLoadArrayDetail(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))
+		case "start-resync":
+			// Activate a RAID device, require "dev=md0" as a query parameter
+			raidManager.HandleSyncPendingToReadWrite(w, r)
 			return
 		default:
 			http.Error(w, "Not Found", http.StatusNotFound)

+ 64 - 0
start.go

@@ -1,8 +1,11 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
+	"io"
 	"io/fs"
+	"log"
 	"net/http"
 	"os"
 
@@ -90,6 +93,67 @@ func initialization() error {
 	return nil
 }
 
+// tmplateMiddleware is a middleware that serves HTML files and injects the CSRF token
+func tmplMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		csrfToken := csrf.Token(r)
+
+		// Check if the request is for a path or ends with .html
+		if r.URL.Path == "/" || r.URL.Path[len(r.URL.Path)-5:] == ".html" {
+			file, err := webfs.Open(r.URL.Path)
+			if err != nil {
+				http.NotFound(w, r)
+				return
+			}
+			defer file.Close()
+
+			// Check if the file is a directory
+			fileInfo, err := file.Stat()
+			if err != nil {
+				log.Println(err)
+				http.Error(w, "Error retrieving file information", http.StatusInternalServerError)
+				return
+			}
+
+			if fileInfo.IsDir() {
+				// If the file is a directory, try to open /index.html
+				indexFile, err := webfs.Open(r.URL.Path + "/index.html")
+				if err != nil {
+					http.NotFound(w, r)
+					return
+				}
+				defer indexFile.Close()
+				file = indexFile
+			}
+
+			// Replace {{.csrfToken}} in the HTML file with the CSRF token
+			content, err := io.ReadAll(file)
+			if err != nil {
+				log.Println(err)
+				http.Error(w, "Error reading file content", http.StatusInternalServerError)
+				return
+			}
+
+			// Replace {{.csrfToken}} with the actual CSRF token
+			modifiedContent := bytes.Replace(content, []byte("{{.csrfToken}}"), []byte(csrfToken), -1)
+
+			// Write the modified content to the response
+			w.Header().Set("Content-Type", "text/html; charset=utf-8")
+			w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+			w.Header().Set("Pragma", "no-cache")
+			w.Header().Set("Expires", "0")
+			w.WriteHeader(http.StatusOK)
+			w.Write(modifiedContent)
+			return
+		}
+
+		next.ServeHTTP(w, r)
+
+		// Add template engine initialization here if needed
+
+	})
+}
+
 // Cleanup function to be called on exit
 func cleanup() {
 	fmt.Println("Performing cleanup tasks...")

+ 7 - 0
web/components/disks.html

@@ -87,6 +87,13 @@
                                         </div>`;
                     }
 
+                    if (disk.partitions.length == 0){
+                        partitionTabs = `<a class="item is-disabled" i18n> 
+                                            No Partitions
+                                            // 無分割區
+                                        </a>`;
+                    }
+
                     $(diskList).append(`<div class="ts-box ts-content has-top-spaced-small disk-info">
                             <div class="ts-grid mobile:is-stacked">
                                 <div class="column is-fluid">

+ 263 - 140
web/components/raid.html

@@ -1,3 +1,9 @@
+<style>
+    #activate_raid_btn{
+        background: var(--ts-positive-400) !important; 
+        border: 0px solid transparent !important;
+    }
+</style>
 <div class="ts-content">
     <div class="ts-container is-padded">
         <div class="ts-grid mobile:is-stacked">
@@ -38,16 +44,7 @@
     </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',
@@ -59,136 +56,29 @@
                     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);
+                        let raidDetails = renderRAIDPoolDetail(raid, index);
+                        $("#raid_array_list").append(raidDetails[0]);
+                        $('#raid_details').append(raidDetails[1]);
                     });
+
+                    if (data.length == 0){
+                        $('#raid_array_list').append(`
+                        <div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
+                            <div class="description"> 
+                                <span class="ts-icon is-circle-check-icon" style="color: var(--ts-positive-400);"></span>
+                                <span class="has-start-spaced-small" i18n> No RAID array found. 
+                                            // 沒有 RAID 陣列
+                                </span>
+                            </div>
+                        </div>`);
+                    }
                 }
 
+                // Show the first RAID details by default
+                if (data.length > 0) {
+                    showRAIDDetails(0);
+                }
                 relocale(); // Recalculate layout
                 syncProgressTicker(); // Start the sync progress ticker
             },
@@ -197,9 +87,243 @@
             }
         });
     }
-
     initRAIDDeviceList();
 
+    function updateRAIDArrayStatus(devname){
+        if (devname.startsWith('/dev/')) {
+            devname = devname.slice(5);
+        }
+        $.ajax({
+            url: './api/raid/info?dev=' + devname,
+            type: 'GET',
+            dataType: 'json',
+            success: function(data) {
+                if (data.error != undefined){
+                    // Handle error response
+                    console.error('Error fetching RAID status:', data.error);
+                    msgbox("Error: " + data.error);
+                    return
+                }
+
+                let menuItem = $(`.raid-array[mdx="${devname}"]`);
+                let raidDetails = $(`.raid-details[mdx="${devname}"]`);
+                let index = menuItem.attr("idx");
+                let domEles = renderRAIDPoolDetail(data, index);
+                let currentShownDetailIndex = 0;
+                if ($(`.raid-array.is-active`).length > 0 && $(`.raid-array.is-active`).attr("idx")){
+                    currentShownDetailIndex = parseInt($(`.raid-array.is-active`).attr("idx"));
+                }
+                menuItem.replaceWith(domEles[0]);
+                raidDetails.replaceWith(domEles[1]);
+                showRAIDDetails(currentShownDetailIndex);
+                syncProgressTicker();
+            },
+            error: function(xhr, status, error) {
+                console.error('Error updating RAID status:', error);
+            }
+        });
+    }
+
+    // Function to render RAID pool details
+    // This function creates the HTML structure for each RAID pool
+    // return the DOM element for the side menu and detail tab
+    function renderRAIDPoolDetail(raid, index){
+        // 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];
+        }
+
+         // Add a new menu item for each RAID array
+         let mdX = raid.DevicePath.split('/').pop();
+         let isSyncing = false;
+         let isResyncPending = false;
+         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;
+                if (raid.State.includes('resyncing') && raid.State.includes('PENDING')) {
+                    //Syncing is pending
+                    isResyncPending = true;
+                    icon = '<span class="ts-icon is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
+                }else{
+                    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>';
+            }
+
+            // Add a new menu item for each RAID array
+            const menuItem = `
+                <a class="raid-array item ${index==0?'is-active':''}" idx="${index}" id="raid_menu_${index}" mdx="${mdX}" 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>
+            `;
+            
+            // Add a hidden div for each RAID array's details
+            const raidDetails = `
+                <div id="raid_details_${index}" mdx="${mdX}" idx="${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>
+                                <!-- For Sync progress -->
+                                ${isSyncing?getRAIDSyncElement(raid, isSyncing):``}
+                                <!-- For RAID Completed -->
+                                ${isResyncPending? getRAIDResumeResyncElement(raid):``}
+                                <span i18n> Array Size
+                                    // 陣列大小
+                                </span>: ${bytesToHumanReadable(raid.ArraySize * 1024)}<br>
+                                <span i18n> Created
+                                    // 建立時間
+                                </span>: <span>${new Date(raid.CreationTime).toLocaleString('en-US', { timeZone: 'UTC' })}</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>
+            `;
+        return [menuItem, raidDetails];
+    }
+
+    function getRAIDSyncElement(raid, isSyncing=true){
+        return `<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>`;
+    }
+
+    // DOM element for RAID resume resync
+    function getRAIDResumeResyncElement(raid){
+        return `<div class="ts-notice has-top-spaced-small has-bottom-spaced-small">
+            <div class="title">
+                <span i18n> RAID Resync Pending
+                    // RAID 重組待處理
+                </span>
+            </div>
+            <div class="content">
+                <span i18n> The previous resync operation was interrupted. Click to resume.
+                    // 先前的重組操作已中斷,點擊以繼續。
+                </span>
+            </div>
+        </div>
+        <button id="activate_raid_btn" onclick="activateSyncPendingDisk('${raid.DevicePath}');" class="ts-button is-fluid has-bottom-spaced-small" i18n> Start Resync
+            // 開始重組
+        </button>`
+    }
+
+    // Function to activate a finished RAID sync
+    // Will set the RAID device to -readwrite state
+    function activateSyncPendingDisk(devname){
+        $.cjax({
+            url: './api/raid/start-resync',
+            method: 'POST',
+            data: { dev: devname},
+            success: function(data) {
+                if (data.error != undefined){
+                    // Handle error response
+                    console.error('Error start resyncing RAID device:', data.error);
+                }else{
+                    // Successfully activated the device
+                    console.log('RAID device resync started successfully:', data);
+                    msgbox(i18nc("raid_resync_started_succ"));
+                    setTimeout(function() {
+                        // Refresh the RAID device list after a short delay
+                        updateRAIDArrayStatus(devname);
+                    }, 300);
+                }
+            },
+        });
+    }
+
     //Create a ticker to check for RAID sync progress
     function syncProgressTicker(){
         let syncProgressTracker = $(".need-update-raid-sync-progress");
@@ -213,10 +337,9 @@
                     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();
+                            // The device is no longer in sync state. Hide the sync progress bar
+                            $(`.sync-progress[devname="${devname}"]`).hide();
+                            $(`.sync-progress[devname="${devname}"]`).removeClass("need-update-raid-sync-progress");
                         }else{
                             let progress = parseFloat(data.ResyncPercent);
                             let total_blocks = parseInt(data.TotalBlocks);

+ 15 - 0
web/index.html

@@ -7,6 +7,7 @@
         Admin Panel | BokoFS
         // 管理介面 | BokoFS
     </title>
+    <meta name="boko.csrf.Token" content="{{.csrfToken}}">
     <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
     <!-- css -->
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
@@ -27,7 +28,21 @@
             z-index: 9999;
         }
     </style>
+    <script>
+        //Add a new function to jquery for ajax override with csrf token injected
+        $.cjax = function(payload){
+            let requireTokenMethod = ["POST", "PUT", "DELETE"];
+            if (requireTokenMethod.includes(payload.method) || requireTokenMethod.includes(payload.type)){
+                //csrf token is required
+                let csrfToken = document.getElementsByTagName("meta")["boko.csrf.Token"].getAttribute("content");
+                payload.headers = {
+                    "X-CSRF-Token": csrfToken,
+                }
+            }
 
+            $.ajax(payload);
+        }
+    </script>
     <link rel="icon" type="image/png" href="img/favicon.png">
 </head>
 <body>

+ 2 - 0
web/js/locale.js

@@ -58,8 +58,10 @@ function i18nc(key, language=undefined){
 let translatedMessages = {
     'en': {
         'disk_info_refreshed': 'Disk information reloaded',
+        "raid_resync_started_succ": 'RAID resync started successfully',
     },
     'zh': {
         'disk_info_refreshed': '磁碟資訊已重新載入',
+        "raid_resync_started_succ": 'RAID 重建已成功啟動',
     }
 };