truenas disk locator

add layout overlay for truenas

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         truenas disk locator
// @namespace    http://tampermonkey.net/
// @version      2024-07-13.2
// @description  add layout overlay for truenas
// @author       You
// @match        https://truenas/*
// @icon         https://truenas/ui/assets/favicons/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==

const locationData = GM_getValue("locationdata") ?? {};

let editmode = true;

const images = locationData.images?.map(imaageStr => {
    const caseImage = new Image();

    caseImage.src = imaageStr;
    caseImage.srcStr = imaageStr;

    return caseImage
})


function getReplySocket(url) {
    const requestMap = {}

    const socket = new WebSocket(url)
    socket.addEventListener("message", message => {
        try {
            const evt = JSON.parse(message.data)
            if(evt.id && requestMap[evt.id]) {
                if(evt.error) {
                    requestMap[evt.id].err(evt)
                } else {
                    requestMap[evt.id].res(evt)
                }
                delete requestMap[evt.id];

            }
        } catch(e) { }
    })
    socket.sendRequest = (request) => {
        return new Promise((res, err) => {
            requestMap[request.id] = { res, err }
            socket.send(JSON.stringify(request))
        })
    }
    return socket
}



const url = location.href;

const authSocket = getReplySocket(`wss://${location.host}/websocket`)
authSocket.addEventListener("open", () => {
    authSocket.send(JSON.stringify({ "msg": "connect", "version": "1", "support": ["1"] }))
})

function startShell() {
    authSocket.sendRequest({
        "id": "09cea36d-3384-8afe-412f-05f99624c9d9",
        "msg": "method",
        "method": "auth.generate_token"
    }).then(token => {
        const shellToken = token.result
        if(!shellToken) {
            debugger;
        }
        const socket = new WebSocket(`wss://${location.host}/websocket/shell/`)
        let echoText = "";
        socket.addEventListener("open", () => {
            socket.send(JSON.stringify({ token: shellToken }));
        })
        socket.addEventListener("message", async (e) => {
            if(typeof e.data == "string" && e.data.includes("connected")) {
                console.log(e.data)

            } else if(e.data instanceof Blob) {
                const text = await e.data.text()
                echoText += text;
                if(echoText.includes("cmd-start") && echoText.split("cmd-end").length > 2) {
                    const disks = echoText.split("total 0")[1].split("cmd-end")[0].split("\r\n").filter(l => l.trim().includes("pci") && !l.includes("-part") && !l.includes(".0 ->"))
                    setDisks(disks);
                    echoText = "";
                } else if(echoText.trim().includes("admin@")) {
                    echoText = "";
                    const getinfoCmd = `echo "cmd-start" && ls -la /dev/disk/by-path && echo "cmd-end" \n`
                    const encoded = new TextEncoder().encode(getinfoCmd);

                    socket.send(encoded)
                }

            }

        })

        socket.addEventListener("close", () => {
            debugger;

        })
    })

}

let diskMap;

function setDisks(disks) {
    diskMap = Object.fromEntries(disks.map(line => {
        const match = line.match(/(?<perms>[lrwx]*) (?<linknum>\d*) (?<owner>[^ ]*) (?<group>[^ ]*) *(?<size>\d*) (?<modmonth>[a-zA-Z]*) (?<modday>\d*) (?<modtime>[\d:]*) (?<path>[^ ]*) -> \.\.\/\.\.\/(?<name>.*)$/)
        return [match.groups.name, match.groups]
    }))
}


authSocket.addEventListener("message", (m) => {
    const evt = JSON.parse(m.data);
    if(evt.msg === "connected") {

        const storageToken = localStorage.getItem("ngx-webstorage|token")
        if(storageToken) {
            const authToken = JSON.parse(storageToken)
            authSocket.sendRequest({
                "id": "5cc307f6-fac4-a746-df80-e6238f2a54e8",
                "msg": "method",
                "method": "auth.token",
                "params": [authToken]
            }).then(resp => {
                if(resp.result == true) {
                    startShell();
                } else {
                    console.warn("token not valid waiting for login")
                    const interv = setInterval(() => {
                        const newStorageToken = localStorage.getItem("ngx-webstorage|token")
                        if(newStorageToken !== storageToken && JSON.parse(newStorageToken) != null) {
                            const authToken = JSON.parse(newStorageToken)
                            clearInterval(interv)
                            authSocket.sendRequest({
                                "id": "5cc307f6-fac4-a746-df80-e6238f2a54e8",
                                "msg": "method",
                                "method": "auth.token",
                                "params": [authToken]
                            }).then(newResp => {
                                if(newResp.result == true) {
                                    startShell();
                                } else {
                                    debugger;
                                }
                            })
                        }
                    }, 100);
                }
            })
        } else {
            debugger;
        }
    } else {


        //debugger;
    }
})


setInterval(() => {

    if(location.pathname.endsWith("/storage/disks") && document.querySelector(".actions-container") && !document.querySelector(".actions-container .insertLBtn") && editmode) {
        const addImageButton = document.createElement("input")
        addImageButton.placeholder = "drag/paste disk layout image here";
        addImageButton.classList.add("insertLBtn")
        addImageButton.addEventListener("paste", async e => {
            const imageTExt = await e.clipboardData.files[0]
            const image = new Image()
            image.src = URL.createObjectURL(imageTExt);
            image.onload = () => {
                const canvas = document.createElement("canvas");
                canvas.width = image.width;
                canvas.height = image.height;

                const context = canvas.getContext("2d");
                context.drawImage(image, 0, 0);

                locationData.images ??= []
                locationData.images.push(canvas.toDataURL())
                GM_setValue("locationdata", locationData)
            }
        })

        document.querySelector(".actions-container").appendChild(addImageButton)

    }
    if(location.pathname.endsWith("/storage/disks") && diskMap) {
        document.querySelectorAll(".mat-sidenav-content #entity-table-component table tbody tr:not(.details-row)").forEach(row => {
            const disk = row.id;
            const path = diskMap[disk].path
            row.title = path
            const modCanvas = document.createElement("canvas");
            row.addEventListener("mouseenter", e => {
                const diskData = locationData.rects[path]
                if(true) {
                    const caseImage = images[diskData?.image ?? 0];
                    modCanvas.remove();
                    modCanvas.width = caseImage.width;
                    modCanvas.height = caseImage.height;

                    modCanvas.style.position = "fixed";
                    modCanvas.style.height = "200px";
                    modCanvas.style.right = "0px";
                    modCanvas.style.zIndex = "9";

                    const context = modCanvas.getContext("2d")
                    context.drawImage(caseImage, 0, 0);
                    row.appendChild(modCanvas)

                    if(diskData?.rect) {
                        try {

                            context.beginPath()
                            context.lineWidth = "2";
                            context.fillStyle = "rgba(0, 255, 0, 0.3)";

                            const pos = diskData.rect[0]
                            const botRight = diskData.rect[1]

                            const rectArgs = [...pos, botRight[0] - pos[0], botRight[1] - pos[1]]
                            context.rect(...rectArgs)
                            context.fill();
                        } catch(e) {

                        }

                    }

                    const ratio = caseImage.height / 200;

                    let topLeft;
                    modCanvas.addEventListener("click", e => {
                        if(!topLeft) {
                            topLeft = [
                                Math.floor(e.offsetX * ratio),
                                Math.floor(e.offsetY * ratio)
                            ]
                        } else if(topLeft) {

                            console.log(`"${path}": [[${topLeft[0]},${topLeft[1]}],[${Math.ceil(e.offsetX * ratio)},${Math.ceil(e.offsetY * ratio)}]]`)

                            locationData.rects ??= {}
                            locationData.rects[path] ??= {}
                            locationData.rects[path].image = diskData?.image ?? 0
                            locationData.rects[path].rect = [
                                topLeft,
                                [
                                    Math.ceil(e.offsetX * ratio),
                                    Math.ceil(e.offsetY * ratio)
                                ]
                            ]
                            GM_setValue("locationdata", locationData)
                            topLeft = undefined
                        }

                    })

                    modCanvas.addEventListener("mousemove", e => {
                        if(topLeft) {
                            const current = [Math.floor(e.offsetX * ratio), Math.floor(e.offsetY * ratio)]
                            context.drawImage(caseImage, 0, 0);


                            context.beginPath()
                            context.lineWidth = "2";
                            context.fillStyle = "rgba(0, 255, 0, 0.3)";

                            const pos = topLeft
                            const botRight = current

                            const rectArgs = [...pos, botRight[0] - topLeft[0], botRight[1] - topLeft[1]]
                            context.rect(...rectArgs)
                            context.fill();
                        }

                    })
                }
            })
            row.addEventListener("mouseleave", e => {
                modCanvas.remove();
            })

            row.querySelectorAll("td:first-child:not(.imageselectadded)").forEach(td => {
                const imageSelector = document.createElement("img")
                imageSelector.style.height = "48px"
                imageSelector.style.width = "48px"
                imageSelector.style.position = "absolute"
                imageSelector.title = "click here to toggle/set image"
                td.classList.add("imageselectadded")

                let imageIndex = 0;

                if(images[imageIndex] && editmode) {
                    imageSelector.src = images[imageIndex].src
                    td.appendChild(imageSelector)
                    imageSelector.onclick = () => {
                        imageIndex++;
                        imageIndex = imageIndex % images.length;
                        imageSelector.src = images[imageIndex].srcStr
                        locationData.rects ??= {}
                        locationData.rects[path] ??= {}
                        locationData.rects[path].image = imageIndex
                        GM_setValue("locationdata", locationData)
                    }
                }
            })
        })
    }
}, 500)