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

This commit is contained in:
Adrian Zürcher
2025-08-06 13:57:34 +02:00
parent e188813adf
commit 9f262c4a55
17 changed files with 428 additions and 564 deletions

107
htop/htopHandler.go Normal file
View 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
View 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
View 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
View 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
View 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 0100
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
View 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>