diff --git a/backend/sysinfo.go b/backend/sysinfo.go index 589d446..a05e3a0 100644 --- a/backend/sysinfo.go +++ b/backend/sysinfo.go @@ -3,6 +3,7 @@ package backend import ( "context" "os/exec" + "sort" "strconv" "strings" "time" @@ -34,15 +35,16 @@ func (s *SysInfo) startEmitter() { return case <-ticker.C: info, _ := s.CollectOnce() - // try to attach GPU info if available + // attach GPU global stats name, tot, used, util := queryGPU(s.ctx) if name != "" { info.GPUName = name info.GPUTotal = tot info.GPUUsed = used info.GPUUtil = util + // attach per-process GPU stats + info.GPUProcesses = queryGPUProcesses(s.ctx) } - // emit via Wails runtime runtime.EventsEmit(s.ctx, "metrics", info) } } @@ -55,16 +57,26 @@ type ProcessInfo struct { Mem uint64 `json:"mem"` } +// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon. +type GPUProcessInfo struct { + PID int32 `json:"pid"` + Name string `json:"name"` + Type string `json:"type"` // "C", "G", or "C+G" + VRAM uint64 `json:"vram_mb"` // framebuffer memory in MB + RAM uint64 `json:"ram"` // resident set size in bytes +} + type Metrics struct { - CPUPercent float64 `json:"cpu_percent"` - TotalMem uint64 `json:"total_mem"` - FreeMem uint64 `json:"free_mem"` - Processes []ProcessInfo `json:"processes"` - Timestamp int64 `json:"timestamp"` - GPUName string `json:"gpu_name,omitempty"` - GPUTotal uint64 `json:"gpu_total_mem,omitempty"` - GPUUsed uint64 `json:"gpu_used_mem,omitempty"` - GPUUtil float64 `json:"gpu_util_percent,omitempty"` + CPUPercent float64 `json:"cpu_percent"` + TotalMem uint64 `json:"total_mem"` + FreeMem uint64 `json:"free_mem"` + Processes []ProcessInfo `json:"processes"` + Timestamp int64 `json:"timestamp"` + GPUName string `json:"gpu_name,omitempty"` + GPUTotal uint64 `json:"gpu_total_mem,omitempty"` + GPUUsed uint64 `json:"gpu_used_mem,omitempty"` + GPUUtil float64 `json:"gpu_util_percent,omitempty"` + GPUProcesses []GPUProcessInfo `json:"gpu_processes,omitempty"` } // CollectOnce gathers a snapshot of system metrics. @@ -72,13 +84,10 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false) vm, _ := mem.VirtualMemory() - // collect top processes by CPU (sample few) + // Collect ALL processes, then sort and keep top 50 procs, _ := ps.Processes() var list []ProcessInfo - for i, p := range procs { - if i >= 30 { - break - } + for _, p := range procs { name, _ := p.Name() cpuPct, _ := p.CPUPercent() memInfo, _ := p.MemoryInfo() @@ -88,6 +97,13 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { } list = append(list, ProcessInfo{PID: p.Pid, Name: name, CPU: cpuPct, Mem: memBytes}) } + // Sort: primary by CPU desc, secondary by Mem desc + sort.Slice(list, func(i, j int) bool { + if list[i].CPU != list[j].CPU { + return list[i].CPU > list[j].CPU + } + return list[i].Mem > list[j].Mem + }) var cpuVal float64 if len(cpuPercents) > 0 { @@ -107,6 +123,52 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { }, nil } +// queryGPUProcesses lists all processes currently using the GPU via nvidia-smi pmon. +func queryGPUProcesses(ctx context.Context) []GPUProcessInfo { + cmd := exec.CommandContext(ctx, "nvidia-smi", "pmon", "-s", "m", "-c", "1") + out, err := cmd.Output() + if err != nil { + return nil + } + var result []GPUProcessInfo + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + fields := strings.Fields(line) + // expected: [gpuIdx, pid, type, fbMB, ccpmMB, name...] + if len(fields) < 5 { + continue + } + pid64, err := strconv.ParseInt(fields[1], 10, 32) + if err != nil { + continue + } + vram, _ := strconv.ParseUint(fields[3], 10, 64) + name := "" + if len(fields) >= 6 { + name = strings.Join(fields[5:], " ") + } else { + name = fields[len(fields)-1] + } + gp := GPUProcessInfo{ + PID: int32(pid64), + Name: name, + Type: fields[2], + VRAM: vram, + } + // look up RAM (RSS) for this process + if p, err := ps.NewProcess(int32(pid64)); err == nil { + if mi, err := p.MemoryInfoWithContext(ctx); err == nil && mi != nil { + gp.RAM = mi.RSS + } + } + result = append(result, gp) + } + return result +} + // try to query GPU info via nvidia-smi; returns name, totalMB, usedMB, utilPercent func queryGPU(ctx context.Context) (string, uint64, uint64, float64) { // Use nvidia-smi if available @@ -138,3 +200,108 @@ func queryGPU(ctx context.Context) (string, uint64, uint64, float64) { func (s *SysInfo) GetMetrics() (*Metrics, error) { return s.CollectOnce() } +// ProcessDetail contains all available details about a single process. +type ProcessDetail struct { + PID int32 `json:"pid"` + Name string `json:"name"` + Exe string `json:"exe"` + Cmdline string `json:"cmdline"` + Cwd string `json:"cwd"` + Status string `json:"status"` + Username string `json:"username"` + CreatedAt int64 `json:"created_at"` // unix ms + ParentPID int32 `json:"parent_pid"` + Nice int32 `json:"nice"` + NumThreads int32 `json:"num_threads"` + NumFDs int32 `json:"num_fds"` + CPUPercent float64 `json:"cpu_percent"` + RSS uint64 `json:"rss"` + VMS uint64 `json:"vms"` + Swap uint64 `json:"swap"` + MemPercent float32 `json:"mem_percent"` + ReadBytes uint64 `json:"read_bytes"` + WriteBytes uint64 `json:"write_bytes"` + ReadOps uint64 `json:"read_ops"` + WriteOps uint64 `json:"write_ops"` + OpenFilesCount int `json:"open_files_count"` + ConnCount int `json:"conn_count"` +} + +// FetchProcessDetail collects all available details for a given PID. +// Fields that can't be read (e.g. permission denied) are left at zero/empty. +func FetchProcessDetail(pid int32) (*ProcessDetail, error) { + p, err := ps.NewProcess(pid) + if err != nil { + return nil, err + } + d := &ProcessDetail{PID: pid} + if v, e := p.Name(); e == nil { + d.Name = v + } + if v, e := p.Exe(); e == nil { + d.Exe = v + } + if v, e := p.Cmdline(); e == nil { + d.Cmdline = v + } + if v, e := p.Cwd(); e == nil { + d.Cwd = v + } + if vv, e := p.Status(); e == nil && len(vv) > 0 { + d.Status = vv[0] + } + if v, e := p.Username(); e == nil { + d.Username = v + } + if v, e := p.CreateTime(); e == nil { + d.CreatedAt = v + } + if v, e := p.Ppid(); e == nil { + d.ParentPID = v + } + if v, e := p.Nice(); e == nil { + d.Nice = v + } + if v, e := p.NumThreads(); e == nil { + d.NumThreads = v + } + if v, e := p.NumFDs(); e == nil { + d.NumFDs = v + } + if v, e := p.CPUPercent(); e == nil { + d.CPUPercent = v + } + if v, e := p.MemoryInfo(); e == nil && v != nil { + d.RSS = v.RSS + d.VMS = v.VMS + d.Swap = v.Swap + } + if v, e := p.MemoryPercent(); e == nil { + d.MemPercent = v + } + if v, e := p.IOCounters(); e == nil && v != nil { + d.ReadBytes = v.ReadBytes + d.WriteBytes = v.WriteBytes + d.ReadOps = v.ReadCount + d.WriteOps = v.WriteCount + } + if v, e := p.OpenFiles(); e == nil { + d.OpenFilesCount = len(v) + } + if v, e := p.Connections(); e == nil { + d.ConnCount = len(v) + } + return d, nil +} + +// KillProcess sends SIGTERM to the process. Use force=true to send SIGKILL. +func KillProcess(pid int32, force bool) error { + p, err := ps.NewProcess(pid) + if err != nil { + return err + } + if force { + return p.Kill() + } + return p.Terminate() +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5b6d9f9..e1d26c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,20 +8,38 @@ "name": "sysmon-frontend", "version": "0.0.1", "dependencies": { + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.378.0", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0", + "tailwind-merge": "^2.6.1" }, "devDependencies": { + "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.0.0", "postcss": "^8.0.0", - "tailwindcss": "^4.0.0", + "tailwindcss": "^3.4.0", "typescript": "^5.1.0", "vite": "^5.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -746,6 +764,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1155,6 +1211,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1205,6 +1272,34 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -1255,6 +1350,32 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -1290,6 +1411,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001777", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", @@ -1311,6 +1442,75 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1318,6 +1518,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1343,6 +1556,20 @@ } } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", @@ -1399,6 +1626,59 @@ "node": ">=6" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1428,6 +1708,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1438,6 +1728,105 @@ "node": ">=6.9.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1470,6 +1859,26 @@ "node": ">=6" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1492,6 +1901,39 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.378.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.378.0.tgz", + "integrity": "sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1499,6 +1941,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1525,6 +1979,43 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1532,6 +2023,39 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -1562,6 +2086,133 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -1569,6 +2220,27 @@ "dev": true, "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1605,6 +2277,61 @@ "node": ">=0.10.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -1650,6 +2377,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1679,12 +2430,181 @@ "node": ">=0.10.0" } }, - "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/typescript": { "version": "5.9.3", @@ -1700,6 +2620,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -1731,6 +2658,13 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 133c719..91e7d5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,17 +8,22 @@ "preview": "vite preview" }, "dependencies": { + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.378.0", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0", + "tailwind-merge": "^2.6.1" }, "devDependencies": { + "@types/node": "^20.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "typescript": "^5.1.0", - "vite": "^5.0.0", "@vitejs/plugin-react": "^5.0.0", - "tailwindcss": "^4.0.0", + "autoprefixer": "^10.0.0", "postcss": "^8.0.0", - "autoprefixer": "^10.0.0" + "tailwindcss": "^3.4.0", + "typescript": "^5.1.0", + "vite": "^5.0.0" } } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 2719a24..000680a 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -f1342afc80cffcb2c13d3ea5fe75c35d \ No newline at end of file +2a33c55ea617ea7dc270228763d7ace6 \ No newline at end of file diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs index 90d9fff..33ad091 100644 --- a/frontend/postcss.config.cjs +++ b/frontend/postcss.config.cjs @@ -1,5 +1,6 @@ module.exports = { plugins: { + tailwindcss: {}, autoprefixer: {}, }, } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5da8c49..95f4321 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,13 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback, useRef } from 'react' import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime' +import { Activity, Cpu, Database, Monitor, ChevronDown, ChevronRight, Layers, X, RefreshCw, OctagonX } from 'lucide-react' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' +import { Progress } from '@/components/ui/progress' +import { cn } from '@/lib/utils' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- type ProcessInfo = { pid: number @@ -8,6 +16,14 @@ type ProcessInfo = { mem: number } +type GPUProcessInfo = { + pid: number + name: string + type: string + vram_mb: number + ram: number +} + type Metrics = { cpu_percent: number total_mem: number @@ -18,10 +34,396 @@ type Metrics = { gpu_total_mem?: number gpu_used_mem?: number gpu_util_percent?: number + gpu_processes?: GPUProcessInfo[] } +type ProcessDetail = { + pid: number + name: string + exe: string + cmdline: string + cwd: string + status: string + username: string + created_at: number + parent_pid: number + nice: number + num_threads: number + num_fds: number + cpu_percent: number + rss: number + vms: number + swap: number + mem_percent: number + read_bytes: number + write_bytes: number + read_ops: number + write_ops: number + open_files_count: number + conn_count: number +} + +type SortField = 'cpu' | 'mem' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function fmtBytes(bytes: number): string { + if (bytes <= 0) return '0 B' + const gb = bytes / 1024 / 1024 / 1024 + if (gb >= 1) return `${gb.toFixed(1)} GB` + const mb = bytes / 1024 / 1024 + if (mb >= 1) return `${mb.toFixed(0)} MB` + return `${(bytes / 1024).toFixed(0)} KB` +} + +function fmtTime(ts: number): string { + return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) +} + +function fmtRelative(ts: number): string { + const secs = Math.floor((Date.now() - ts) / 1000) + if (secs < 60) return `${secs}s ago` + const mins = Math.floor(secs / 60) + if (mins < 60) return `${mins}m ago` + const hrs = Math.floor(mins / 60) + const remMins = mins % 60 + if (hrs < 24) return remMins > 0 ? `${hrs}h ${remMins}m ago` : `${hrs}h ago` + const days = Math.floor(hrs / 24) + return `${days}d ago` +} + +function fmtStatus(code: string): string { + const map: Record = { + R: 'Running', S: 'Sleeping', D: 'Disk wait', Z: 'Zombie', + T: 'Stopped', t: 'Tracing', X: 'Dead', I: 'Idle', + } + return map[code] ?? code +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function MetricValue({ value, pct }: { value: string; pct: number }) { + return ( + = 90 ? 'text-red-400' : pct >= 70 ? 'text-amber-400' : 'text-zinc-100' + )} + > + {value} + + ) +} + +function DetailRow({ label, value, mono = false }: { label: string; value: React.ReactNode; mono?: boolean }) { + return ( +
+ {label} + {value || '—'} +
+ ) +} + +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function StatBlock({ label, value, sub }: { label: string; value: React.ReactNode; sub?: string }) { + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ) +} + +// --------------------------------------------------------------------------- +// Process detail panel +// --------------------------------------------------------------------------- + +function ProcessDetailPanel({ + pid, + onClose, +}: { + pid: number + onClose: () => void +}) { + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(true) + const [killing, setKilling] = useState(false) + const [killConfirm, setKillConfirm] = useState(false) + const [killError, setKillError] = useState(null) + const intervalRef = useRef | null>(null) + + const load = useCallback(async () => { + try { + // Use low-level Wails IPC — binding will be regenerated automatically + const d = await (window as any)['go']['main']['App']['GetProcessDetail'](pid) as ProcessDetail + setDetail(d) + } catch { + setDetail(null) + } finally { + setLoading(false) + } + }, [pid]) + + useEffect(() => { + load() + intervalRef.current = setInterval(load, 2000) + return () => { if (intervalRef.current) clearInterval(intervalRef.current) } + }, [load]) + + const killProcess = useCallback(async (force: boolean) => { + setKilling(true) + setKillError(null) + try { + await (window as any)['go']['main']['App']['KillProcess'](pid, force) + // stop refreshing — process is gone + if (intervalRef.current) clearInterval(intervalRef.current) + onClose() + } catch (e: any) { + setKillError(String(e?.message ?? e)) + setKillConfirm(false) + } finally { + setKilling(false) + } + }, [pid, onClose]) + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [onClose]) + + return ( + <> + {/* Backdrop */} +
+ + {/* Panel */} +
+ {/* Panel header */} +
+
+
+ + {detail?.name ?? `PID ${pid}`} + + {detail?.status && ( + + {fmtStatus(detail.status)} + + )} +
+

+ PID {pid} + {detail?.username ? ` · ${detail.username}` : ''} + {detail?.created_at ? ` · started ${fmtRelative(detail.created_at)}` : ''} +

+
+
+ + + {/* Kill button */} + {!killConfirm ? ( + + ) : ( +
+ Sure? + + + +
+ )} + + +
+
+ + {/* Scrollable content */} +
+ {loading && !detail ? ( +

Loading…

+ ) : !detail ? ( +

Process not found or permission denied.

+ ) : ( + <> + {killError && ( +
+ Kill failed: {killError} +
+ )} + {/* Identity */} + Identity +
+ 0 ? detail.parent_pid : '—'} /> + + +
+ + {/* Paths */} + Executable +
+ + + {detail.cwd && } +
+ + {/* CPU & Memory */} + CPU & Memory +
+ + +
+
+ + + +
+ + {/* I/O */} + {(detail.read_bytes > 0 || detail.write_bytes > 0) && ( + <> + Disk I/O +
+ + +
+ + )} + + {/* Files & Network */} + Files & Network +
+ + +
+ + )} +
+ +
+

Auto-refreshes every 2s · Press Esc to close

+
+
+ + ) +} + +// --------------------------------------------------------------------------- +// Main App +// --------------------------------------------------------------------------- + export default function App() { const [metrics, setMetrics] = useState(null) + const [sortBy, setSortBy] = useState('cpu') + const [selectedPid, setSelectedPid] = useState(null) + const [page, setPage] = useState(0) + const [gpuPage, setGpuPage] = useState(0) + const [procOpen, setProcOpen] = useState(true) + const [gpuOpen, setGpuOpen] = useState(true) + const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram') + + const PAGE_SIZE = 20 + const GPU_PAGE_SIZE = 4 useEffect(() => { const handler = (m: Metrics) => setMetrics(m) @@ -29,79 +431,405 @@ export default function App() { return () => EventsOff('metrics') }, []) - // No simulator: real metrics only + const toggleSort = useCallback((field: SortField) => { + setSortBy(field) + setPage(0) + }, []) + + const usedMem = metrics ? metrics.total_mem - metrics.free_mem : 0 + const memPct = metrics ? (usedMem / metrics.total_mem) * 100 : 0 + const cpuPct = metrics?.cpu_percent ?? 0 + const gpuUsedPct = + metrics?.gpu_total_mem && metrics?.gpu_used_mem + ? (metrics.gpu_used_mem / metrics.gpu_total_mem) * 100 + : 0 + const gpuUtilPct = metrics?.gpu_util_percent ?? 0 + + const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) => + gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram + ) + const gpuTotalPages = Math.ceil(gpuProcesses.length / GPU_PAGE_SIZE) + const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1)) + const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * GPU_PAGE_SIZE, (safeGpuPage + 1) * GPU_PAGE_SIZE) + + const sortedProcesses = metrics + ? [...metrics.processes].sort((a, b) => b[sortBy] - a[sortBy]) + : [] + const totalPages = Math.ceil(sortedProcesses.length / PAGE_SIZE) + const safePage = Math.min(page, Math.max(0, totalPages - 1)) + const pagedProcesses = sortedProcesses.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE) return ( -
-
-

sysmon

-
Realtime system monitor
+
+ {/* Header */} +
+
+ + sysmon +
+
+ + + {metrics ? fmtTime(metrics.timestamp) : 'connecting…'} + +
-
-
-

Overview

- {metrics ? ( -
-
CPU: {metrics.cpu_percent.toFixed(1)}%
-
Memory: {((metrics.total_mem - metrics.free_mem) / (1024 * 1024)).toFixed(0)} MB used
-
Memory free: {(metrics.free_mem / (1024 * 1024)).toFixed(0)} MB
- {metrics.processes && metrics.processes.length > 0 && ( +
+ {/* Processes */} + + setProcOpen(o => !o)} + > +
+
+ {procOpen + ? + : + }
- Top memory: { - (() => { - const top = [...metrics.processes].sort((a, b) => b.mem - a.mem)[0] - return `${top.name} (${(top.mem / 1024 / 1024).toFixed(1)} MB)` - })() - } + Processes + {procOpen &&

Click a row for details

}
- )} -
Last: {new Date(metrics.timestamp).toLocaleTimeString()}
+
+
e.stopPropagation()}> + {/* Summary values always visible */} +
+
+

CPU

+ = 90 ? 'text-red-400' : cpuPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{cpuPct.toFixed(1)}% +
+
+

Memory

+ = 90 ? 'text-red-400' : memPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{fmtBytes(usedMem)} +
+
+ {procOpen && ( +
+ {(['cpu', 'mem'] as SortField[]).map((field) => ( + + ))} +
+ )} +
- ) : ( -
Waiting for metrics…
- )} -
- -
-

GPU

- {metrics ? ( -
-
Name: {metrics.gpu_name || 'N/A'}
-
GPU Memory: {metrics.gpu_used_mem ? (metrics.gpu_used_mem / 1024 / 1024).toFixed(0) + ' MB used / ' + (metrics.gpu_total_mem! / 1024 / 1024).toFixed(0) + ' MB' : 'N/A'}
-
Util: {metrics.gpu_util_percent ? metrics.gpu_util_percent.toFixed(1) + '%' : 'N/A'}
+ + {procOpen && ( + + {/* System resource bars */} +
+
+
+ CPU + = 90 ? 'text-red-400' : cpuPct >= 70 ? 'text-amber-400' : 'text-zinc-400')}>{cpuPct.toFixed(1)}% +
+ +
+
+
+ Memory + = 90 ? 'text-red-400' : memPct >= 70 ? 'text-amber-400' : 'text-zinc-400')}>{fmtBytes(usedMem)} / {metrics ? fmtBytes(metrics.total_mem) : '—'} +
+ +
- ) : ( -
Waiting for GPU…
- )} -
- -
-

Top processes

-
- +
- - - - - + + + + + - {metrics?.processes.map((p) => ( - - - - - + {pagedProcesses.map((p) => ( + setSelectedPid(p.pid)} + className={cn( + 'border-b border-zinc-900 transition-colors cursor-pointer', + selectedPid === p.pid + ? 'bg-zinc-800/60' + : 'hover:bg-zinc-900/60' + )} + > + + + + ))} + {pagedProcesses.length === 0 && ( + + + + )}
PIDNameCPUMem
PIDNameCPUMemory
{p.pid}{p.name}{p.cpu.toFixed(1)}%{(p.mem / 1024 / 1024).toFixed(1)} MB
{p.pid}{p.name} + = 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500' + )} + > + {p.cpu.toFixed(1)}% + + + + {fmtBytes(p.mem)} + +
+ Waiting for metrics… +
-
-
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + {safePage * PAGE_SIZE + 1}–{Math.min((safePage + 1) * PAGE_SIZE, sortedProcesses.length)} of {sortedProcesses.length} + +
+ + + {Array.from({ length: totalPages }, (_, i) => i) + .filter(i => i === 0 || i === totalPages - 1 || Math.abs(i - safePage) <= 1) + .reduce<(number | '…')[]>((acc, i, idx, arr) => { + if (idx > 0 && typeof arr[idx - 1] === 'number' && (i as number) - (arr[idx - 1] as number) > 1) acc.push('…') + acc.push(i) + return acc + }, []) + .map((item, idx) => + item === '…' ? ( + + ) : ( + + ) + ) + } + + +
+
+ )} + + )} + + + {/* GPU */} + {metrics?.gpu_name && ( + + setGpuOpen(o => !o)} + > +
+
+ {gpuOpen + ? + : + } + + GPU + + {metrics.gpu_name} + +
+
e.stopPropagation()}> +
+

VRAM

+ = 90 ? 'text-red-400' : gpuUsedPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'} +
+
+

Util

+ = 90 ? 'text-red-400' : gpuUtilPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{gpuUtilPct.toFixed(0)}% +
+ {gpuOpen && ( +
+ {(['vram', 'ram'] as const).map((field) => ( + + ))} +
+ )} +
+
+
+ {gpuOpen && ( + +
+
+
+ VRAM usage + + {metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'} + {' / '} + {metrics.gpu_total_mem ? fmtBytes(metrics.gpu_total_mem) : '—'} + +
+ +
+
+
+ GPU utilization + {gpuUtilPct.toFixed(1)}% +
+ +
+
+ + {metrics.gpu_processes && metrics.gpu_processes.length > 0 && ( +
+
+
+ + + GPU Processes + + {gpuProcesses.length} total +
+
+ + + + + + + + + + + {pagedGpuProcesses.map((p) => ( + setSelectedPid(p.pid)} + > + + + + + + ))} + +
NameTypeVRAMRAM
{p.name} + + {p.type} + + {p.vram_mb} MB{p.ram > 0 ? fmtBytes(p.ram) : '—'}
+ + {/* GPU pagination */} + {gpuTotalPages > 1 && ( +
+ + {safeGpuPage * GPU_PAGE_SIZE + 1}–{Math.min((safeGpuPage + 1) * GPU_PAGE_SIZE, gpuProcesses.length)} of {gpuProcesses.length} + +
+ + + {Array.from({ length: gpuTotalPages }, (_, i) => i) + .filter(i => i === 0 || i === gpuTotalPages - 1 || Math.abs(i - safeGpuPage) <= 1) + .reduce<(number | '…')[]>((acc, i, idx, arr) => { + if (idx > 0 && typeof arr[idx - 1] === 'number' && (i as number) - (arr[idx - 1] as number) > 1) acc.push('…') + acc.push(i) + return acc + }, []) + .map((item, idx) => + item === '…' ? ( + + ) : ( + + ) + ) + } + + +
+
+ )} +
+ )} +
+ )} +
+ )}
+ + {/* Detail panel */} + {selectedPid !== null && ( + setSelectedPid(null)} + /> + )}
) } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..8baf0e4 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardContent.displayName = 'CardContent' + +export { Card, CardHeader, CardTitle, CardContent } diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx new file mode 100644 index 0000000..e3a5112 --- /dev/null +++ b/frontend/src/components/ui/progress.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +interface ProgressProps extends React.HTMLAttributes { + value: number + max?: number + colorThresholds?: { warn: number; crit: number } +} + +function getBarColor(pct: number, warn: number, crit: number): string { + if (pct >= crit) return 'bg-red-500' + if (pct >= warn) return 'bg-amber-400' + return 'bg-emerald-500' +} + +const Progress = React.forwardRef( + ({ value, max = 100, colorThresholds = { warn: 70, crit: 90 }, className, ...props }, ref) => { + const pct = Math.min(100, Math.max(0, (value / max) * 100)) + const { warn, crit } = colorThresholds + return ( +
+
+
+ ) + } +) +Progress.displayName = 'Progress' + +export { Progress } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2c101d2..50b6e8e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,15 +1,14 @@ -:root{ - --neon: #39ff14; +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + * { + box-sizing: border-box; + } + + body { + @apply bg-zinc-950 text-zinc-100 antialiased; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; + } } - -body{font-family: Inter, ui-sans-serif, system-ui; background:#000; color:#e6eef0} -.text-neon{color:var(--neon)} - -/* Minimal layout for dev without Tailwind */ -.app{min-height:100vh} -header{padding:1.5rem; display:flex; align-items:center; justify-content:space-between} -main{padding:1.5rem; display:grid; grid-template-columns:1fr 2fr; gap:1.5rem} -section{background:#071422; padding:1rem; border-radius:0.5rem; border:1px solid rgba(43,45,66,0.4)} -table{width:100%; border-collapse:collapse} -thead tr{opacity:0.85; font-size:0.9rem} -tbody tr:nth-child(odd){background:rgba(0,0,0,0.08)} diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index afda031..4a891a3 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -1,11 +1,17 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { + darkMode: ['class'], content: ['./index.html', './src/**/*.{ts,tsx}'], theme: { extend: { - colors: { - neon: '#39ff14' - } - } + fontFamily: { + sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], + mono: ['JetBrains Mono', 'ui-monospace', 'monospace'], + }, + animation: { + 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + }, }, - plugins: [] + plugins: [], } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..bc931c6 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ed0bbbd..4ee6134 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,11 +1,17 @@ import { defineConfig } from 'vite' +import path from 'path' export default defineConfig(async () => { const reactPlugin = (await import('@vitejs/plugin-react')).default return { plugins: [reactPlugin()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, build: { - outDir: '../frontend/dist' - } + outDir: '../frontend/dist', + }, } }) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 074c1fa..2b2a37f 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -5,6 +5,10 @@ import {context} from '../models'; export function GetMetrics():Promise; +export function GetProcessDetail(arg1:number):Promise; + export function Greet(arg1:string):Promise; export function InitBackend(arg1:context.Context):Promise; + +export function KillProcess(arg1:number,arg2:boolean):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index fe02abe..e1b2519 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -6,6 +6,10 @@ export function GetMetrics() { return window['go']['main']['App']['GetMetrics'](); } +export function GetProcessDetail(arg1) { + return window['go']['main']['App']['GetProcessDetail'](arg1); +} + export function Greet(arg1) { return window['go']['main']['App']['Greet'](arg1); } @@ -13,3 +17,7 @@ export function Greet(arg1) { export function InitBackend(arg1) { return window['go']['main']['App']['InitBackend'](arg1); } + +export function KillProcess(arg1, arg2) { + return window['go']['main']['App']['KillProcess'](arg1, arg2); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index f5dce98..2c8d73e 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,5 +1,25 @@ export namespace backend { + export class GPUProcessInfo { + pid: number; + name: string; + type: string; + vram_mb: number; + ram: number; + + static createFrom(source: any = {}) { + return new GPUProcessInfo(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.pid = source["pid"]; + this.name = source["name"]; + this.type = source["type"]; + this.vram_mb = source["vram_mb"]; + this.ram = source["ram"]; + } + } export class ProcessInfo { pid: number; name: string; @@ -28,6 +48,7 @@ export namespace backend { gpu_total_mem?: number; gpu_used_mem?: number; gpu_util_percent?: number; + gpu_processes?: GPUProcessInfo[]; static createFrom(source: any = {}) { return new Metrics(source); @@ -44,6 +65,7 @@ export namespace backend { this.gpu_total_mem = source["gpu_total_mem"]; this.gpu_used_mem = source["gpu_used_mem"]; this.gpu_util_percent = source["gpu_util_percent"]; + this.gpu_processes = this.convertValues(source["gpu_processes"], GPUProcessInfo); } convertValues(a: any, classs: any, asMap: boolean = false): any { @@ -64,6 +86,62 @@ export namespace backend { return a; } } + export class ProcessDetail { + pid: number; + name: string; + exe: string; + cmdline: string; + cwd: string; + status: string; + username: string; + created_at: number; + parent_pid: number; + nice: number; + num_threads: number; + num_fds: number; + cpu_percent: number; + rss: number; + vms: number; + swap: number; + mem_percent: number; + read_bytes: number; + write_bytes: number; + read_ops: number; + write_ops: number; + open_files_count: number; + conn_count: number; + + static createFrom(source: any = {}) { + return new ProcessDetail(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.pid = source["pid"]; + this.name = source["name"]; + this.exe = source["exe"]; + this.cmdline = source["cmdline"]; + this.cwd = source["cwd"]; + this.status = source["status"]; + this.username = source["username"]; + this.created_at = source["created_at"]; + this.parent_pid = source["parent_pid"]; + this.nice = source["nice"]; + this.num_threads = source["num_threads"]; + this.num_fds = source["num_fds"]; + this.cpu_percent = source["cpu_percent"]; + this.rss = source["rss"]; + this.vms = source["vms"]; + this.swap = source["swap"]; + this.mem_percent = source["mem_percent"]; + this.read_bytes = source["read_bytes"]; + this.write_bytes = source["write_bytes"]; + this.read_ops = source["read_ops"]; + this.write_ops = source["write_ops"]; + this.open_files_count = source["open_files_count"]; + this.conn_count = source["conn_count"]; + } + } }