-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4353a4e
commit 5960ad2
Showing
23 changed files
with
718 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,51 @@ | ||
/* eslint-disable padding-line-between-statements */ | ||
"use client"; | ||
|
||
import { useEffect } from "react"; | ||
import React, { useState, useEffect } from "react"; | ||
|
||
import { useDetectCookie } from "@/hooks/useDetectCookie"; | ||
// Widgets | ||
import CPUWidget from "@/components/dashboard/cpu-widget"; | ||
import DiskWidget from "@/components/dashboard/disk-widget"; | ||
import MemoryWidget from "@/components/dashboard/memory-widget"; | ||
import GPUWidget from "@/components/dashboard/gpu-widget"; | ||
import BatteryWidget from "@/components/dashboard/battery-widget"; | ||
import OSWidget from "@/components/dashboard/os-widget"; | ||
import { WebSocketContext } from "@/hooks/useOS"; | ||
|
||
export default function Page() { | ||
const [mounted, setMounted] = useState<boolean>(false); | ||
const [ws, setWebSocket] = useState<WebSocket | null>(null); | ||
|
||
useEffect(() => { | ||
setMounted(true); | ||
|
||
document.title = "Ferrum - 仪表盘"; | ||
|
||
const _ws = new WebSocket(`ws://${window.location.host}/api/os`); | ||
setWebSocket(_ws); | ||
|
||
return () => _ws?.close(); | ||
}, []); | ||
|
||
useEffect(() => { | ||
return () => ws?.close(); | ||
}, [ws]); | ||
|
||
useDetectCookie(); | ||
|
||
return <></>; | ||
if(!mounted) return <></>; | ||
|
||
return ( | ||
<WebSocketContext.Provider value={{ ws }}> | ||
<div className="w-[1000px] h-[600px] mx-auto b-15 p-5 grid grid-cols-4 grid-rows-3 gap-4"> | ||
<CPUWidget className="col-span-2"/> | ||
<DiskWidget className="col-span-2 row-span-2"/> | ||
<MemoryWidget className="col-span-2"/> | ||
<GPUWidget className="col-span-2"/> | ||
<BatteryWidget /> | ||
<OSWidget /> | ||
</div> | ||
</WebSocketContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* eslint-disable no-console */ | ||
import type { IncomingMessage } from "http"; | ||
import type { WebSocket, WebSocketServer } from "ws"; | ||
import type { OSWebSocketMessage } from "@/hooks/useOS"; | ||
|
||
import os from "os"; | ||
|
||
import si from "systeminformation"; | ||
import { cpuModel, usagePercent as cpuPercent } from "node-system-stats"; | ||
import cookie from "cookie"; | ||
|
||
import { error } from "@/lib/packet"; | ||
import { tokenStorageKey } from "@/lib/global"; | ||
import { validateToken } from "@/lib/token"; | ||
|
||
export function GET() { | ||
return error(400); | ||
} | ||
|
||
export function SOCKET( | ||
client: WebSocket, | ||
req: IncomingMessage, | ||
_server: WebSocketServer, | ||
) { | ||
const token = cookie.parse(req.headers.cookie ?? "")[tokenStorageKey]; | ||
|
||
if(!token) { | ||
client.close(401); | ||
|
||
return; | ||
} | ||
if(!validateToken(token)) { | ||
client.close(403); | ||
|
||
return; | ||
} | ||
|
||
console.log("[Server: /api/os] Socket client connected."); | ||
|
||
const handleRequest = async () => { | ||
const cpu = await si.cpu(); | ||
const cpuTemp = await si.cpuTemperature(); | ||
const mem = await si.mem(); | ||
const memLayout = await si.memLayout(); | ||
const graphics = await si.graphics(); | ||
const battery = await si.battery(); | ||
const disk = await si.fsSize(); | ||
|
||
// if(cpuTemp.main === null) { | ||
// console.warn("CPU temperature info on Windows requires Administrator privilege."); | ||
// } | ||
|
||
client.send(JSON.stringify({ | ||
cpu: { | ||
model: cpuModel, | ||
totalCores: cpu.cores, | ||
usage: (await cpuPercent()).percent, | ||
|
||
/** | ||
* Admin privilege required on Windows | ||
* @see https://systeminformation.io/cpu.html | ||
*/ | ||
temperature: cpuTemp.main | ||
}, | ||
memory: { | ||
total: mem.total, | ||
usage: (mem.used / mem.total) * 100, | ||
amount: memLayout.length | ||
}, | ||
gpu: graphics.controllers.map((gpu) => ({ | ||
model: gpu.model, | ||
vendor: gpu.vendor, | ||
memoryTotal: gpu.vram, | ||
memoryUsage: gpu.memoryUsed && gpu.memoryTotal ? ((gpu.memoryUsed / gpu.memoryTotal) * 100) : undefined | ||
})), | ||
battery: { | ||
hasBattery: battery.hasBattery, | ||
isCharging: battery.isCharging, | ||
percent: battery.percent | ||
}, | ||
disk: disk.map((item) => ({ | ||
mount: item.mount, | ||
type: item.type, | ||
size: item.size, | ||
used: item.used | ||
})), | ||
os: { | ||
arch: os.arch(), | ||
platform: os.platform(), | ||
release: os.release(), | ||
version: os.version() | ||
} | ||
} as OSWebSocketMessage)); | ||
}; | ||
|
||
var timer = setInterval(handleRequest, 5000); | ||
|
||
handleRequest(); | ||
|
||
client.on("close", () => { | ||
clearInterval(timer); | ||
|
||
console.log("[Server: /api/os] Socket client disconnected."); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import type { PropsWithCN } from "@/types"; | ||
|
||
import React, { useState } from "react"; | ||
import { Progress } from "@nextui-org/progress"; | ||
|
||
import DashboardWidget from "./dashboard-widget"; | ||
|
||
import { type BatteryInfo, useOS } from "@/hooks/useOS"; | ||
|
||
const BatteryWidget: React.FC<PropsWithCN> = (props) => { | ||
const [batteryInfo, setBatteryInfo] = useState<BatteryInfo | null>(null); | ||
|
||
useOS(({ battery }) => { | ||
setBatteryInfo(battery); | ||
}); | ||
|
||
return ( | ||
<DashboardWidget | ||
name="电池状态" | ||
className={props.className} | ||
insideClassName="flex flex-col justify-between"> | ||
{ | ||
batteryInfo?.hasBattery | ||
? ( | ||
<> | ||
<p className="text-green-700 dark:text-green-500">{batteryInfo?.isCharging ? "正在充电" : ""}</p> | ||
|
||
<div className="flex flex-col gap-4"> | ||
<span className="text-3xl font-semibold">{batteryInfo?.percent ? `${batteryInfo?.percent}%` : "--%"}</span> | ||
|
||
<Progress | ||
classNames={{ indicator: "bg-green-600 dark:bg-green-500" }} | ||
size="sm" | ||
value={batteryInfo?.percent} | ||
aria-label="电池电量"/> | ||
</div> | ||
</> | ||
) | ||
: <p className="text-default-500">未发现电池</p> | ||
} | ||
</DashboardWidget> | ||
); | ||
} | ||
|
||
export default BatteryWidget; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import type { PropsWithCN } from "@/types"; | ||
|
||
import React, { useState } from "react"; | ||
import { Progress } from "@nextui-org/progress"; | ||
|
||
import DashboardWidget from "./dashboard-widget"; | ||
|
||
import { type CPUInfo, useOS } from "@/hooks/useOS"; | ||
|
||
const CPUWidget: React.FC<PropsWithCN> = (props) => { | ||
const [cpuInfo, setCPUInfo] = useState<CPUInfo | null>(null); | ||
|
||
useOS(({ cpu }) => { | ||
setCPUInfo(cpu); | ||
}); | ||
|
||
return ( | ||
<DashboardWidget | ||
name="CPU 占用" | ||
className={props.className} | ||
insideClassName="flex flex-col justify-between"> | ||
<p className="text-default-500">{cpuInfo?.model} {cpuInfo?.totalCores ? `(${cpuInfo.totalCores}核)` : ""}</p> | ||
|
||
<div className="flex flex-col gap-4"> | ||
<div className="flex justify-between items-end"> | ||
<span className=""> | ||
<span className="text-default-500">温度:</span> | ||
{cpuInfo?.temperature ? `${cpuInfo?.temperature.toFixed(2)}°C` : "--°C"} | ||
</span> | ||
<span className="text-3xl font-semibold">{cpuInfo?.usage ? `${cpuInfo?.usage}%` : "--%"}</span> | ||
</div> | ||
|
||
<Progress | ||
size="sm" | ||
value={cpuInfo?.usage} | ||
aria-label="CPU 占用"/> | ||
</div> | ||
</DashboardWidget> | ||
); | ||
} | ||
|
||
export default CPUWidget; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { PropsWithCN } from "@/types"; | ||
|
||
import React, { type PropsWithChildren } from "react"; | ||
import { Card } from "@nextui-org/card"; | ||
import { cn } from "@nextui-org/theme"; | ||
|
||
interface DashboardWidgetProps extends PropsWithCN, PropsWithChildren { | ||
name: string; | ||
insideClassName?: string | ||
} | ||
|
||
const DashboardWidget: React.FC<DashboardWidgetProps> = (props) => { | ||
return ( | ||
<Card className={cn("px-5 py-4 flex flex-col gap-2", props.className)}> | ||
<span className="text-xl font-semibold">{props.name}</span> | ||
|
||
<div className={cn("flex-1", props.insideClassName)}> | ||
{props.children} | ||
</div> | ||
</Card> | ||
); | ||
} | ||
|
||
export default DashboardWidget; |
Oops, something went wrong.