more abstraction and simple dashboard for processes
All checks were successful
Build Process Supervisor / build (amd64, .exe, windows) (push) Successful in 2m24s
Build Process Supervisor / build (amd64, , linux) (push) Successful in 2m36s
Build Process Supervisor / build (arm, 6, , linux) (push) Successful in 2m19s
Build Process Supervisor / build (arm64, , linux) (push) Successful in 2m14s
All checks were successful
Build Process Supervisor / build (amd64, .exe, windows) (push) Successful in 2m24s
Build Process Supervisor / build (amd64, , linux) (push) Successful in 2m36s
Build Process Supervisor / build (arm, 6, , linux) (push) Successful in 2m19s
Build Process Supervisor / build (arm64, , linux) (push) Successful in 2m14s
This commit is contained in:
107
htop/htopHandler.go
Normal file
107
htop/htopHandler.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package htop
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
var templatesFS embed.FS
|
||||
|
||||
type HTopHandler struct {
|
||||
Table *HtopTable
|
||||
}
|
||||
|
||||
func NewHTopHandler() (*HTopHandler, error) {
|
||||
table, err := NewTable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTopHandler{
|
||||
Table: table,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HTopHandler) UpdateHTop(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.Table.UpdateTable()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get processes", 500)
|
||||
return
|
||||
}
|
||||
h.Table.Sort(r)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"div": divide,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Detect HTMX request via the HX-Request header
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
tmpl := template.Must(
|
||||
template.New("table.html").Funcs(funcMap).ParseFS(
|
||||
templatesFS,
|
||||
"templates/table.html",
|
||||
),
|
||||
)
|
||||
tmpl.Execute(w, h.Table)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(
|
||||
template.New("htop.html").Funcs(funcMap).ParseFS(
|
||||
templatesFS,
|
||||
"templates/htop.html",
|
||||
"templates/table.html",
|
||||
),
|
||||
)
|
||||
tmpl.Execute(w, h.Table)
|
||||
}
|
||||
|
||||
func divide(a any, b any) float64 {
|
||||
af := toFloat64(a)
|
||||
bf := toFloat64(b)
|
||||
if bf == 0 {
|
||||
return 0
|
||||
}
|
||||
return af / bf
|
||||
}
|
||||
|
||||
func toFloat64(v any) float64 {
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return float64(val)
|
||||
case int32:
|
||||
return float64(val)
|
||||
case int64:
|
||||
return float64(val)
|
||||
case uint64:
|
||||
return float64(val)
|
||||
case float32:
|
||||
return float64(val)
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// GET /taskmanager/usage
|
||||
func UsageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cpu, mem, err := GetSystemUsage()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get usage", 500)
|
||||
return
|
||||
}
|
||||
|
||||
usage := struct {
|
||||
CPU float64 `json:"cpu"`
|
||||
Mem float64 `json:"mem"`
|
||||
}{
|
||||
CPU: cpu,
|
||||
Mem: mem,
|
||||
}
|
||||
json.NewEncoder(w).Encode(usage)
|
||||
}
|
102
htop/htopTable.go
Normal file
102
htop/htopTable.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package htop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"processSupervisor/htop/models"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
type HtopTable struct {
|
||||
Processes []models.Process
|
||||
CurrentSort string
|
||||
CurrentOrder string
|
||||
}
|
||||
|
||||
func NewTable() (*HtopTable, error) {
|
||||
processes, err := models.GetProcesses()
|
||||
if err != nil {
|
||||
return &HtopTable{}, err
|
||||
}
|
||||
return &HtopTable{
|
||||
Processes: processes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HtopTable) UpdateTable() error {
|
||||
var err error
|
||||
t.Processes, err = models.GetProcesses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HtopTable) Sort(r *http.Request) {
|
||||
sortBy := r.URL.Query().Get("sort")
|
||||
order := r.URL.Query().Get("order")
|
||||
|
||||
if order == "desc" {
|
||||
t.CurrentOrder = "desc"
|
||||
} else {
|
||||
t.CurrentOrder = "asc"
|
||||
}
|
||||
|
||||
t.CurrentSort = sortBy
|
||||
|
||||
sort.Slice(t.Processes, func(i, j int) bool {
|
||||
switch sortBy {
|
||||
case "pid":
|
||||
pidI, _ := strconv.Atoi(t.Processes[i].PID)
|
||||
pidJ, _ := strconv.Atoi(t.Processes[j].PID)
|
||||
if t.CurrentOrder == "desc" {
|
||||
return pidI > pidJ
|
||||
}
|
||||
return pidI < pidJ
|
||||
case "user":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].User > t.Processes[j].User
|
||||
}
|
||||
return t.Processes[i].User < t.Processes[j].User
|
||||
case "cmd":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].Cmd > t.Processes[j].Cmd
|
||||
}
|
||||
return t.Processes[i].Cmd < t.Processes[j].Cmd
|
||||
case "cpu":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].CPU > t.Processes[j].CPU
|
||||
}
|
||||
return t.Processes[i].CPU < t.Processes[j].CPU
|
||||
case "memory":
|
||||
if t.CurrentOrder == "desc" {
|
||||
return t.Processes[i].Memory > t.Processes[j].Memory
|
||||
}
|
||||
return t.Processes[i].Memory < t.Processes[j].Memory
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func GetSystemUsage() (cpuPercent float64, memPercent float64, err error) {
|
||||
cpuPercents, err := cpu.Percent(0, false)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if len(cpuPercents) == 0 {
|
||||
return 0, 0, fmt.Errorf("cpu.Percent returned empty slice")
|
||||
}
|
||||
|
||||
vmStat, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return cpuPercents[0], vmStat.UsedPercent, nil
|
||||
}
|
40
htop/killHandler.go
Normal file
40
htop/killHandler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package htop
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"processSupervisor/htop/models"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func KillHandler(w http.ResponseWriter, r *http.Request) {
|
||||
pidStr := r.URL.Query().Get("pid")
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid PID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
http.Error(w, "Process not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = proc.Kill()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to kill process", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated table partial (same as HTMX request)
|
||||
processes, err := models.GetProcesses()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get processes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.ParseFiles("templates/table.html"))
|
||||
tmpl.Execute(w, processes)
|
||||
}
|
58
htop/models/process.go
Normal file
58
htop/models/process.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ps "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
PID string
|
||||
User string
|
||||
Cmd string
|
||||
CPU float64
|
||||
Memory uint64
|
||||
}
|
||||
|
||||
func GetProcesses() ([]Process, error) {
|
||||
processes, err := ps.Processes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []Process
|
||||
for _, p := range processes {
|
||||
pid := p.Pid
|
||||
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
user, err := p.Username()
|
||||
if err != nil {
|
||||
user = "unknown"
|
||||
}
|
||||
|
||||
cpu, err := p.CPUPercent()
|
||||
if err != nil {
|
||||
cpu = 0
|
||||
}
|
||||
|
||||
memInfo, err := p.MemoryInfo()
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, Process{
|
||||
PID: fmt.Sprintf("%d", pid),
|
||||
User: user,
|
||||
Cmd: name,
|
||||
CPU: cpu,
|
||||
Memory: memInfo.RSS,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
195
htop/templates/htop.html
Normal file
195
htop/templates/htop.html
Normal file
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Task Manager</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--text-color: #e0e0e0;
|
||||
--accent-color: #007bff;
|
||||
--danger-color: #dc3545;
|
||||
--table-bg: #1e1e1e;
|
||||
--hover-bg: #2a2a2a;
|
||||
--border-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
font-size: 2.8rem;
|
||||
margin: 2rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(to right, #00c6ff, #00e6aa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1e1e2f; /* dark card background */
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
|
||||
margin: 3rem auto; /* vertical margin + horizontal centering */
|
||||
max-width: 900px; /* limits width */
|
||||
color: #e0e0ff;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--table-bg);
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #80dfff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.usage-bar {
|
||||
position: relative;
|
||||
background-color: #333;
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.usage-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: #00e676; /* initial */
|
||||
transition: width 0.5s ease, background-color 0.4s ease;
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.usage-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
user-select: none;
|
||||
text-shadow: 0 0 5px rgba(0,0,0,0.7);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="system-stats" class="card" style="margin-bottom: 1.5rem;">
|
||||
<div>
|
||||
<label>CPU Usage</label>
|
||||
<div class="usage-bar">
|
||||
<div id="cpu-bar" class="usage-fill"></div>
|
||||
<span id="cpu-percent" class="usage-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<label>Memory Usage</label>
|
||||
<div class="usage-bar">
|
||||
<div id="mem-bar" class="usage-fill"></div>
|
||||
<span id="mem-percent" class="usage-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="page-title">🧠 Task Manager</h1>
|
||||
<div id="process-table" class="card">
|
||||
{{template "table.html" .}}
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
function updateUsage() {
|
||||
fetch("/taskmanager/usage")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const cpu = Math.round(data.cpu);
|
||||
const mem = Math.round(data.mem);
|
||||
|
||||
updateBar('cpu-bar', 'cpu-percent', cpu);
|
||||
updateBar('mem-bar', 'mem-percent', mem);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function updateBar(barId, textId, percent) {
|
||||
const bar = document.getElementById(barId);
|
||||
const text = document.getElementById(textId);
|
||||
|
||||
bar.style.width = percent + '%';
|
||||
text.textContent = percent + '%';
|
||||
|
||||
// Set interpolated color from green to red
|
||||
bar.style.backgroundColor = getInterpolatedColor(percent);
|
||||
}
|
||||
|
||||
function getInterpolatedColor(percent) {
|
||||
// Clamp between 0–100
|
||||
percent = Math.max(0, Math.min(100, percent));
|
||||
|
||||
let r, g;
|
||||
|
||||
if (percent <= 50) {
|
||||
// green (0%) to yellow (50%)
|
||||
r = Math.floor(255 * (percent / 50)); // 0 → 255
|
||||
g = 230; // stay green
|
||||
} else {
|
||||
// yellow (50%) to red (100%)
|
||||
r = 255;
|
||||
g = Math.floor(230 - 230 * ((percent - 50) / 50)); // 230 → 0
|
||||
}
|
||||
|
||||
return `rgb(${r},${g},0)`;
|
||||
}
|
||||
|
||||
setInterval(updateUsage, 5000);
|
||||
updateUsage();
|
||||
</script>
|
||||
</html>
|
74
htop/templates/table.html
Normal file
74
htop/templates/table.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<div id="process-table" class="card"
|
||||
hx-get="/taskmanager/htop?sort={{.CurrentSort}}&order={{.CurrentOrder}}"
|
||||
hx-trigger="every 2s"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
<table >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=pid&order={{if and (eq .CurrentSort "pid") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
PID {{if eq .CurrentSort "pid"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=user&order={{if and (eq .CurrentSort "user") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
User {{if eq .CurrentSort "user"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=cmd&order={{if and (eq .CurrentSort "cmd") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
Command {{if eq .CurrentSort "cmd"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=cpu&order={{if and (eq .CurrentSort "cpu") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
CPU %{{if eq .CurrentSort "cpu"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a hx-get="/taskmanager/htop?sort=memory&order={{if and (eq .CurrentSort "memory") (eq .CurrentOrder "asc")}}desc{{else}}asc{{end}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="innerHTML">
|
||||
Memory {{if eq .CurrentSort "memory"}}{{if eq .CurrentOrder "asc"}}↑{{else}}↓{{end}}{{end}}
|
||||
</a>
|
||||
</th>
|
||||
<th>Kill</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Processes}}
|
||||
<tr>
|
||||
<td class="pid">{{.PID}}</td>
|
||||
<td class="user">{{.User}}</td>
|
||||
<td class="cmd">{{.Cmd}}</td>
|
||||
<td class="cpu">{{printf "%.2f" .CPU}}</td>
|
||||
<td class="mem">
|
||||
{{if ge .Memory 1073741824}}
|
||||
{{printf "%.2f GB" (div .Memory 1073741824)}}
|
||||
{{else}}
|
||||
{{printf "%.0f MB" (div .Memory 1048576)}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="kill">
|
||||
<button
|
||||
hx-get="/taskmanager/kill?pid={{.PID}}"
|
||||
hx-target="#process-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to kill {{.Cmd}} process?">
|
||||
Kill
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
Reference in New Issue
Block a user