您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
add layout overlay for truenas
// ==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)
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址