premier commit
This commit is contained in:
22
inventory/Dockerfile
Normal file
22
inventory/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Image golang pour la compilation
|
||||
FROM golang:1.25 AS builder
|
||||
|
||||
# Copier les fichiers dont ont a besoin
|
||||
COPY src /go/src
|
||||
COPY Makefile /go
|
||||
|
||||
# Compilation
|
||||
RUN make build
|
||||
|
||||
# Image finale
|
||||
FROM busybox
|
||||
|
||||
# Installer le logiciel compilé
|
||||
COPY src/www /www
|
||||
COPY --from=builder /go/bin/inventory /inventory
|
||||
|
||||
# Partages
|
||||
EXPOSE 80
|
||||
|
||||
# Démarrage
|
||||
ENTRYPOINT [ "/inventory" ]
|
||||
17
inventory/Makefile
Normal file
17
inventory/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
run:
|
||||
@cd src ; go run *.go
|
||||
|
||||
build:
|
||||
@cd src ; go mod tidy ; go build -o ../bin/inventory *.go
|
||||
|
||||
clean:
|
||||
@rm ./bin/inventory
|
||||
|
||||
image:
|
||||
@docker build -t inventory:latest .
|
||||
|
||||
start:
|
||||
@docker run -d -p 80:80 --name inventory inventory:latest
|
||||
|
||||
stop:
|
||||
@docker rm -f inventory
|
||||
51
inventory/src/cpu/cpu.go
Normal file
51
inventory/src/cpu/cpu.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package cpu
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
)
|
||||
|
||||
type CPUCore struct {
|
||||
Info cpu.InfoStat `json:"info"`
|
||||
Usage float64 `json:"usage_percent"`
|
||||
Times *cpu.TimesStat `json:"times,omitempty"`
|
||||
}
|
||||
|
||||
type CPUInfo struct {
|
||||
Cores []CPUCore `json:"cores"`
|
||||
}
|
||||
|
||||
func ReadCPU() (*CPUInfo, error) {
|
||||
infos, err := cpu.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usage, err := cpu.Percent(0, true) // usage par core
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
times, err := cpu.Times(true) // stats par core
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]CPUCore, 0, len(infos))
|
||||
for i, info := range infos {
|
||||
core := CPUCore{
|
||||
Info: info,
|
||||
}
|
||||
|
||||
if i < len(usage) {
|
||||
core.Usage = usage[i]
|
||||
}
|
||||
if i < len(times) {
|
||||
tmp := times[i] // adresse stable
|
||||
core.Times = &tmp
|
||||
}
|
||||
|
||||
out = append(out, core)
|
||||
}
|
||||
|
||||
return &CPUInfo{Cores: out}, nil
|
||||
}
|
||||
38
inventory/src/disk/disk.go
Normal file
38
inventory/src/disk/disk.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
type DiskFS struct {
|
||||
Partition disk.PartitionStat `json:"partition"`
|
||||
Usage *disk.UsageStat `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
func ReadDisk() (*[]DiskFS, error) {
|
||||
parts, err := disk.Partitions(true) // true = toutes les partitions
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []DiskFS
|
||||
|
||||
for _, p := range parts {
|
||||
fs := DiskFS{Partition: p}
|
||||
|
||||
// Usage peut échouer (ex: pseudo-fs, permissions)
|
||||
if u, err := disk.Usage(p.Mountpoint); err == nil {
|
||||
fs.Usage = u
|
||||
}
|
||||
|
||||
// Exclusion des FS spécifique au système
|
||||
switch fs.Partition.Fstype {
|
||||
case "none", "proc", "tmpfs", "overlay", "sysfs", "cgroup2", "mqueue", "nsfs":
|
||||
// ignoré
|
||||
default:
|
||||
out = append(out, fs)
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
16
inventory/src/go.mod
Normal file
16
inventory/src/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/evoliatis/buildup
|
||||
|
||||
go 1.25.6
|
||||
|
||||
require github.com/shirou/gopsutil/v4 v4.26.1
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
)
|
||||
32
inventory/src/go.sum
Normal file
32
inventory/src/go.sum
Normal file
@@ -0,0 +1,32 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
108
inventory/src/handle.go
Normal file
108
inventory/src/handle.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/evoliatis/buildup/cpu"
|
||||
"github.com/evoliatis/buildup/disk"
|
||||
"github.com/evoliatis/buildup/load"
|
||||
"github.com/evoliatis/buildup/memory"
|
||||
"github.com/evoliatis/buildup/netcard"
|
||||
"github.com/evoliatis/buildup/proc"
|
||||
)
|
||||
|
||||
func HealthHandle(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "ok")
|
||||
}
|
||||
|
||||
// Liste des processus
|
||||
func PSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
myProcs, err := proc.ReadProc("")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(myProcs)
|
||||
}
|
||||
|
||||
// Liste des processus par user
|
||||
func PSUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.PathValue("user")
|
||||
myProcs, err := proc.ReadProc(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(myProcs)
|
||||
}
|
||||
|
||||
func NetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := netcard.ReadNetwork("")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func NetNameHandler(w http.ResponseWriter, r *http.Request) {
|
||||
card := r.PathValue("card")
|
||||
out, err := netcard.ReadNetwork(card)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func MemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := memory.ReadMemory()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func DiskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := disk.ReadDisk()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func LoadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := load.ReadLoad()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func CPUHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := cpu.ReadCPU()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
27
inventory/src/load/load.go
Normal file
27
inventory/src/load/load.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package load
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
)
|
||||
|
||||
type LoadInfo struct {
|
||||
Avg *load.AvgStat `json:"avg"`
|
||||
Misc *load.MiscStat `json:"misc"`
|
||||
}
|
||||
|
||||
func ReadLoad() (*LoadInfo, error) {
|
||||
avg, err := load.Avg()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
misc, err := load.Misc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LoadInfo{
|
||||
Avg: avg,
|
||||
Misc: misc,
|
||||
}, nil
|
||||
}
|
||||
12
inventory/src/main.go
Normal file
12
inventory/src/main.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
log.Println("listening on :80")
|
||||
log.Fatal(http.ListenAndServe(":80", router()))
|
||||
}
|
||||
27
inventory/src/memory/memory.go
Normal file
27
inventory/src/memory/memory.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
)
|
||||
|
||||
type MemInfo struct {
|
||||
Virtual *mem.VirtualMemoryStat `json:"virtual"`
|
||||
Swap *mem.SwapMemoryStat `json:"swap"`
|
||||
}
|
||||
|
||||
func ReadMemory() (*MemInfo, error) {
|
||||
vm, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sm, err := mem.SwapMemory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MemInfo{
|
||||
Virtual: vm,
|
||||
Swap: sm,
|
||||
}, nil
|
||||
}
|
||||
42
inventory/src/netcard/netcard.go
Normal file
42
inventory/src/netcard/netcard.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package netcard
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
type NetCard struct {
|
||||
Interface net.InterfaceStat `json:"interface"`
|
||||
IO *net.IOCountersStat `json:"io,omitempty"`
|
||||
}
|
||||
|
||||
func ReadNetwork(nom string) (*[]NetCard, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counters, err := net.IOCounters(true) // true => stats par interface
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// index des compteurs par nom d'interface
|
||||
byName := make(map[string]net.IOCountersStat, len(counters))
|
||||
for _, c := range counters {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
// fusion
|
||||
out := make([]NetCard, 0, len(ifaces))
|
||||
for _, itf := range ifaces {
|
||||
card := NetCard{Interface: itf}
|
||||
if c, ok := byName[itf.Name]; ok {
|
||||
tmp := c
|
||||
card.IO = &tmp
|
||||
}
|
||||
if nom == "" || nom == card.Interface.Name {
|
||||
out = append(out, card)
|
||||
}
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
35
inventory/src/proc/proc.go
Normal file
35
inventory/src/proc/proc.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package proc
|
||||
|
||||
import "github.com/shirou/gopsutil/v4/process"
|
||||
|
||||
type Proc struct {
|
||||
Pid int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
User string `json:"user"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory float32 `json:"memory"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// Chargement des Processus
|
||||
func ReadProc(user string) (*[]Proc, error) {
|
||||
var myProcs []Proc
|
||||
procs, err := process.Processes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, proc := range procs {
|
||||
var myProc Proc
|
||||
myProc.User, _ = proc.Username()
|
||||
if user == "" || user == myProc.User {
|
||||
myProc.Pid = proc.Pid
|
||||
myProc.Name, _ = proc.Name()
|
||||
myProc.CPU, _ = proc.CPUPercent()
|
||||
myProc.Memory, _ = proc.MemoryPercent()
|
||||
status, _ := proc.Status()
|
||||
myProc.Status = status[0]
|
||||
myProcs = append(myProcs, myProc)
|
||||
}
|
||||
}
|
||||
return &myProcs, nil
|
||||
}
|
||||
27
inventory/src/routes.go
Normal file
27
inventory/src/routes.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Routeur pour l'ensemble de nos routes
|
||||
|
||||
func router() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
// Routes de test
|
||||
mux.HandleFunc("GET /health", HealthHandle)
|
||||
|
||||
// Routes techniques
|
||||
mux.HandleFunc("GET /cpu", CPUHandler)
|
||||
mux.HandleFunc("GET /ps", PSHandler)
|
||||
mux.HandleFunc("GET /ps/{user}", PSUserHandler)
|
||||
mux.HandleFunc("GET /net", NetHandler)
|
||||
mux.HandleFunc("GET /net/{card}", NetNameHandler)
|
||||
mux.HandleFunc("GET /mem", MemHandler)
|
||||
mux.HandleFunc("GET /disk", DiskHandler)
|
||||
mux.HandleFunc("GET /load", LoadHandler)
|
||||
|
||||
// Autres cas : fichiers statiques
|
||||
fs := http.FileServer(http.Dir("www"))
|
||||
mux.Handle("/", fs)
|
||||
|
||||
return mux
|
||||
}
|
||||
147
inventory/src/www/disk.html
Normal file
147
inventory/src/www/disk.html
Normal file
@@ -0,0 +1,147 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Disques · volumes & points de montage</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin:0; padding:24px; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:canvas; color:canvastext; }
|
||||
.card { max-width:1100px; margin:0 auto 16px; padding:16px 20px; border:1px solid color-mix(in oklab, canvastext 15%, transparent); border-radius:16px; background:color-mix(in oklab, canvas 92%, canvastext 0%); box-shadow:0 1px 8px color-mix(in oklab, canvastext 10%, transparent); }
|
||||
h1 { margin:0 0 8px; font-size:1.25rem; }
|
||||
.muted { opacity:.75; font-size:.9rem; display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
|
||||
.pill { display:inline-block; padding:.15rem .5rem; border-radius:999px; border:1px solid color-mix(in oklab, canvastext 18%, transparent); }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
th, td { padding:8px 10px; border-bottom:1px solid color-mix(in oklab, canvastext 12%, transparent); text-align:left; vertical-align:top; }
|
||||
th { font-weight:600; white-space:nowrap; }
|
||||
.mono { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size:.9rem; }
|
||||
.badges { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.badge { display:inline-block; padding:.12rem .45rem; border-radius:.5rem; border:1px solid color-mix(in oklab, canvastext 25%, transparent); font-size:.85rem; }
|
||||
.right { text-align:right; white-space:nowrap; }
|
||||
.w-usage { min-width:120px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="card">
|
||||
<h1>Disques / partitions</h1>
|
||||
<div class="muted">
|
||||
<span class="pill">Source: <span class="mono">/disk</span></span>
|
||||
<span class="pill">Refresh: <span class="mono">5s</span></span>
|
||||
<span class="pill">Dernière mise à jour: <span id="updatedAt" class="mono">—</span></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Mountpoint</th>
|
||||
<th>Type</th>
|
||||
<th>Options</th>
|
||||
<th class="right">Total</th>
|
||||
<th class="right">Utilisé</th>
|
||||
<th class="right">Libre</th>
|
||||
<th class="right w-usage">Utilisé %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
<tr><td colspan="8" class="muted">Chargement…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const DISK_URL = '/disk';
|
||||
const REFRESH_INTERVAL = 5000; // 5 secondes
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"')
|
||||
.replace(/'/g,''');
|
||||
}
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (typeof n !== 'number' || !isFinite(n) || n < 0) return '—';
|
||||
const units = ['B','KiB','MiB','GiB','TiB','PiB'];
|
||||
let u = 0;
|
||||
let v = n;
|
||||
while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; }
|
||||
const digits = (u === 0) ? 0 : (u <= 2 ? 1 : 2);
|
||||
return v.toFixed(digits) + ' ' + units[u];
|
||||
}
|
||||
|
||||
function fmtPercent(n) {
|
||||
if (typeof n !== 'number' || !isFinite(n)) return '—';
|
||||
return n.toFixed(1) + ' %';
|
||||
}
|
||||
|
||||
function renderRows(items) {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return '<tr><td colspan="8" class="muted">Aucune donnée</td></tr>';
|
||||
}
|
||||
|
||||
return items.map(function(x){
|
||||
const p = x && x.partition ? x.partition : {};
|
||||
const u = x && x.usage ? x.usage : null;
|
||||
|
||||
const opts = Array.isArray(p.opts) ? p.opts : [];
|
||||
const optsHtml = opts.length
|
||||
? ('<div class="badges">' + opts.map(function(o){
|
||||
return '<span class="badge">' + esc(o) + '</span>';
|
||||
}).join('') + '</div>')
|
||||
: '<span class="muted">—</span>';
|
||||
|
||||
const total = u ? fmtBytes(u.total) : '—';
|
||||
const used = u ? fmtBytes(u.used) : '—';
|
||||
const free = u ? fmtBytes(u.free) : '—';
|
||||
const usedPct = u ? fmtPercent(u.usedPercent) : '—';
|
||||
|
||||
return '<tr>'
|
||||
+ '<td class="mono">' + esc(p.device || '') + '</td>'
|
||||
+ '<td class="mono">' + esc(p.mountpoint || '') + '</td>'
|
||||
+ '<td>' + esc(p.fstype || '') + '</td>'
|
||||
+ '<td>' + optsHtml + '</td>'
|
||||
+ '<td class="right mono">' + esc(total) + '</td>'
|
||||
+ '<td class="right mono">' + esc(used) + '</td>'
|
||||
+ '<td class="right mono">' + esc(free) + '</td>'
|
||||
+ '<td class="right mono">' + esc(usedPct) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let inFlight = false;
|
||||
|
||||
async function refresh() {
|
||||
if (inFlight) return;
|
||||
inFlight = true;
|
||||
|
||||
const body = document.getElementById('tbody');
|
||||
try {
|
||||
// Si ton HTML est servi via un reverse-proxy (recommandé), DISK_URL suffit (same-origin).
|
||||
// Si tu dois pointer un serveur distant, remplace par une URL absolue ici.
|
||||
const res = await fetch(DISK_URL, { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
body.innerHTML = renderRows(data);
|
||||
|
||||
const now = new Date();
|
||||
document.getElementById('updatedAt').textContent = now.toLocaleString();
|
||||
} catch (e) {
|
||||
body.innerHTML = '<tr><td colspan="8" class="muted">Erreur: ' + esc(e.message) + '</td></tr>';
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
// chargement immédiat + refresh périodique
|
||||
refresh();
|
||||
setInterval(refresh, REFRESH_INTERVAL);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
inventory/src/www/img/inventory.png
Normal file
BIN
inventory/src/www/img/inventory.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
504
inventory/src/www/index.html
Normal file
504
inventory/src/www/index.html
Normal file
@@ -0,0 +1,504 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="Inventory — inventaire automatisé des postes, rapide, fiable, moderne." />
|
||||
<title>Inventory — Reveleant Software Solution</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg0:#07110d;
|
||||
--bg1:#0a1b14;
|
||||
--card: rgba(255,255,255,.06);
|
||||
--card2: rgba(255,255,255,.09);
|
||||
--stroke: rgba(255,255,255,.12);
|
||||
--text: rgba(255,255,255,.92);
|
||||
--muted: rgba(255,255,255,.72);
|
||||
--green:#38e38d;
|
||||
--green2:#1fbf6a;
|
||||
--shadow: 0 18px 60px rgba(0,0,0,.45);
|
||||
--radius: 18px;
|
||||
--radius2: 26px;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(1100px 700px at 20% 10%, rgba(56,227,141,.20), transparent 55%),
|
||||
radial-gradient(900px 600px at 90% 40%, rgba(31,191,106,.16), transparent 60%),
|
||||
linear-gradient(180deg, var(--bg0), var(--bg1));
|
||||
overflow-x:hidden;
|
||||
}
|
||||
|
||||
a{color:inherit}
|
||||
.wrap{max-width:1100px;margin:0 auto;padding:26px 18px 64px}
|
||||
|
||||
/* Subtle “tech grid” */
|
||||
.grid{
|
||||
position:fixed; inset:0;
|
||||
pointer-events:none;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255,255,255,.06) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255,255,255,.06) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
mask-image: radial-gradient(700px 400px at 30% 20%, black 40%, transparent 85%);
|
||||
opacity:.35;
|
||||
}
|
||||
|
||||
header{
|
||||
display:flex;align-items:center;justify-content:space-between;gap:16px;
|
||||
padding:14px 16px;
|
||||
border:1px solid var(--stroke);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04));
|
||||
border-radius: var(--radius2);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.brand{
|
||||
display:flex;align-items:center;gap:12px;min-width: 200px;
|
||||
}
|
||||
.dot{
|
||||
width:12px;height:12px;border-radius:999px;
|
||||
background: radial-gradient(circle at 30% 30%, #fff, var(--green));
|
||||
box-shadow: 0 0 0 6px rgba(56,227,141,.15), 0 0 26px rgba(56,227,141,.35);
|
||||
}
|
||||
.brand strong{letter-spacing:.4px}
|
||||
.brand span{display:block;color:var(--muted);font-size:12.5px;margin-top:1px}
|
||||
|
||||
nav{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}
|
||||
.pill{
|
||||
text-decoration:none;
|
||||
font-size:13px;
|
||||
padding:9px 12px;
|
||||
border-radius:999px;
|
||||
border:1px solid var(--stroke);
|
||||
background: rgba(255,255,255,.04);
|
||||
}
|
||||
.pill:hover{background: rgba(255,255,255,.07)}
|
||||
.cta{
|
||||
text-decoration:none;
|
||||
font-weight:700;
|
||||
letter-spacing:.2px;
|
||||
padding:10px 14px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(56,227,141,.35);
|
||||
background: linear-gradient(180deg, rgba(56,227,141,.22), rgba(31,191,106,.12));
|
||||
box-shadow: 0 10px 30px rgba(31,191,106,.18);
|
||||
}
|
||||
.cta:hover{filter:brightness(1.05)}
|
||||
|
||||
/* Quick links (2 lignes) */
|
||||
.quick-links{
|
||||
margin-top: 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--stroke);
|
||||
background: rgba(255,255,255,.03);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.quick-links .ql-row{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.quick-links .ql-row + .ql-row{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.quick-links .ql-label{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
|
||||
width:100%;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px 14px;
|
||||
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(56,227,141,.32);
|
||||
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(56,227,141,.22),
|
||||
rgba(31,191,106,.10)
|
||||
);
|
||||
|
||||
color: rgba(255,255,255,.95);
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .3px;
|
||||
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255,255,255,.06),
|
||||
0 6px 18px rgba(31,191,106,.18);
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
.quick-links .ql-row + .ql-row{
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed rgba(56,227,141,.25);
|
||||
}
|
||||
|
||||
|
||||
.quick-links a.ql{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--stroke);
|
||||
background: rgba(255,255,255,.03);
|
||||
color: rgba(255,255,255,.86);
|
||||
text-decoration:none;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.quick-links a.ql:hover{
|
||||
background: rgba(255,255,255,.06);
|
||||
border-color: rgba(56,227,141,.28);
|
||||
}
|
||||
|
||||
.quick-links a.ql code{
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,.72);
|
||||
background: rgba(0,0,0,.14);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero{
|
||||
margin-top:22px;
|
||||
display:grid;
|
||||
grid-template-columns: 1.15fr .85fr;
|
||||
gap:18px;
|
||||
align-items:stretch;
|
||||
}
|
||||
@media (max-width: 900px){
|
||||
.hero{grid-template-columns:1fr}
|
||||
nav{justify-content:flex-start}
|
||||
}
|
||||
.panel{
|
||||
border:1px solid var(--stroke);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.03));
|
||||
border-radius: var(--radius2);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.hero-left{padding:26px}
|
||||
.kicker{
|
||||
display:inline-flex;align-items:center;gap:10px;
|
||||
padding:8px 12px;border-radius:999px;
|
||||
border:1px solid rgba(56,227,141,.28);
|
||||
background: rgba(56,227,141,.09);
|
||||
color: rgba(255,255,255,.86);
|
||||
font-size:13px;
|
||||
}
|
||||
.pulse{
|
||||
width:10px;height:10px;border-radius:999px;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 0 rgba(56,227,141,.45);
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
@keyframes pulse{
|
||||
0%{box-shadow:0 0 0 0 rgba(56,227,141,.45)}
|
||||
70%{box-shadow:0 0 0 14px rgba(56,227,141,0)}
|
||||
100%{box-shadow:0 0 0 0 rgba(56,227,141,0)}
|
||||
}
|
||||
h1{
|
||||
margin:16px 0 10px;
|
||||
font-size:42px;
|
||||
line-height:1.04;
|
||||
letter-spacing:-.6px;
|
||||
}
|
||||
@media (max-width: 520px){
|
||||
h1{font-size:34px}
|
||||
}
|
||||
.lead{
|
||||
margin:0 0 18px;
|
||||
color: var(--muted);
|
||||
font-size:16.5px;
|
||||
line-height:1.55;
|
||||
max-width: 62ch;
|
||||
}
|
||||
.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
|
||||
.btn{
|
||||
display:inline-flex;align-items:center;justify-content:center;gap:10px;
|
||||
text-decoration:none;
|
||||
padding:12px 14px;
|
||||
border-radius: 14px;
|
||||
border:1px solid var(--stroke);
|
||||
background: rgba(255,255,255,.04);
|
||||
font-weight:700;
|
||||
}
|
||||
.btn:hover{background: rgba(255,255,255,.07)}
|
||||
.btn-primary{
|
||||
border-color: rgba(56,227,141,.35);
|
||||
background: linear-gradient(180deg, rgba(56,227,141,.25), rgba(31,191,106,.12));
|
||||
}
|
||||
.btn-primary:hover{filter:brightness(1.05)}
|
||||
.hint{
|
||||
margin-top:12px;
|
||||
color: rgba(255,255,255,.62);
|
||||
font-size:12.5px;
|
||||
}
|
||||
|
||||
/* Hero right: logo “card” */
|
||||
.hero-right{
|
||||
position:relative;
|
||||
padding:0;
|
||||
display:flex;
|
||||
align-items:stretch;
|
||||
justify-content:stretch;
|
||||
min-height: 340px;
|
||||
}
|
||||
.logo-card{
|
||||
position:relative;
|
||||
width:100%;
|
||||
padding:22px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
isolation:isolate;
|
||||
}
|
||||
|
||||
/* Put your Inventory logo file next to this HTML and name it: inventory.png */
|
||||
.logo-image{
|
||||
width:min(420px, 92%);
|
||||
height:auto;
|
||||
filter: drop-shadow(0 18px 42px rgba(0,0,0,.45));
|
||||
border-radius: 10px;
|
||||
}
|
||||
.glow{
|
||||
position:absolute; inset:auto;
|
||||
width:520px; height:520px;
|
||||
background: radial-gradient(circle at 35% 35%, rgba(56,227,141,.28), transparent 55%),
|
||||
radial-gradient(circle at 60% 55%, rgba(31,191,106,.18), transparent 60%);
|
||||
filter: blur(12px);
|
||||
z-index:-1;
|
||||
transform: translateY(-6px);
|
||||
opacity:.9;
|
||||
}
|
||||
.scanline{
|
||||
position:absolute; left:-10%; right:-10%; height:140px;
|
||||
top: 40%;
|
||||
background: linear-gradient(180deg,
|
||||
transparent,
|
||||
rgba(56,227,141,.16),
|
||||
rgba(56,227,141,.08),
|
||||
transparent
|
||||
);
|
||||
transform: skewY(-4deg);
|
||||
animation: sweep 4.8s ease-in-out infinite;
|
||||
mix-blend-mode: screen;
|
||||
pointer-events:none;
|
||||
}
|
||||
@keyframes sweep{
|
||||
0%{top:18%;opacity:.0}
|
||||
18%{opacity:.55}
|
||||
50%{top:62%;opacity:.35}
|
||||
100%{top:18%;opacity:.0}
|
||||
}
|
||||
|
||||
/* Feature grid */
|
||||
.features{
|
||||
margin-top:18px;
|
||||
display:grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap:14px;
|
||||
}
|
||||
@media (max-width: 900px){
|
||||
.features{grid-template-columns:1fr}
|
||||
}
|
||||
.card{
|
||||
padding:18px;
|
||||
border-radius: var(--radius);
|
||||
border:1px solid var(--stroke);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
|
||||
box-shadow: 0 14px 40px rgba(0,0,0,.28);
|
||||
}
|
||||
.card h3{
|
||||
margin:0 0 8px;
|
||||
font-size:16px;
|
||||
letter-spacing:-.2px;
|
||||
display:flex;align-items:center;gap:10px;
|
||||
}
|
||||
.ico{
|
||||
width:34px;height:34px;border-radius:12px;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
border:1px solid rgba(56,227,141,.28);
|
||||
background: rgba(56,227,141,.10);
|
||||
}
|
||||
.card p{margin:0;color:var(--muted);line-height:1.55;font-size:14.5px}
|
||||
|
||||
/* Footer */
|
||||
footer{
|
||||
margin-top:22px;
|
||||
padding:16px 18px;
|
||||
border-radius: var(--radius2);
|
||||
border:1px solid var(--stroke);
|
||||
background: rgba(255,255,255,.03);
|
||||
color: rgba(255,255,255,.68);
|
||||
font-size:13px;
|
||||
display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:space-between;
|
||||
}
|
||||
.fine a{opacity:.9}
|
||||
.fine a:hover{opacity:1}
|
||||
.badge{
|
||||
display:inline-flex;align-items:center;gap:10px;
|
||||
padding:8px 10px;border-radius:999px;
|
||||
border:1px solid rgba(56,227,141,.22);
|
||||
background: rgba(56,227,141,.08);
|
||||
}
|
||||
.mini{
|
||||
width:8px;height:8px;border-radius:999px;background: var(--green);
|
||||
box-shadow: 0 0 18px rgba(56,227,141,.35);
|
||||
}
|
||||
@keyframes heartbeat {
|
||||
0% { transform: scale(1); }
|
||||
14% { transform: scale(1.035); }
|
||||
28% { transform: scale(1); }
|
||||
42% { transform: scale(1.02); }
|
||||
70% { transform: scale(1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.logo-heartbeat {
|
||||
animation: heartbeat 2.6s ease-in-out infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="grid" aria-hidden="true"></div>
|
||||
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="brand" aria-label="Inventory">
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<div>
|
||||
<strong>Inventory</strong>
|
||||
<span>Inventaire automatisé des postes • Agent actif</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Navigation">
|
||||
<a class="pill" href="#fonctionnalites">Fonctionnalités</a>
|
||||
<a class="pill" href="#securite">Sécurité</a>
|
||||
<a class="pill" href="#contact">Contact</a>
|
||||
|
||||
<!-- IMPORTANT : remplacez ce lien par l'URL réelle de l'éditeur -->
|
||||
<a class="cta" href="https://poudreverte.org/" target="_blank" rel="noopener noreferrer">
|
||||
Site de l’éditeur
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero" aria-label="Présentation">
|
||||
<div class="panel hero-left">
|
||||
<div class="kicker"><span class="pulse" aria-hidden="true"></span> Agent en ligne • Collecte temps réel</div>
|
||||
|
||||
<h1>Votre parc, <span style="color:var(--green)">clair</span> et <span style="color:var(--green2)">à jour</span>.</h1>
|
||||
|
||||
<p class="lead">
|
||||
<strong>Inventory</strong> automatise l’inventaire matériel & logiciel des postes sur le réseau :
|
||||
collecte des constantes système, normalisation, historisation, et exposition via API.
|
||||
Une interface moderne, sans lourdeur.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<!-- IMPORTANT : remplacez ce lien par l'URL réelle de l'éditeur -->
|
||||
<a class="btn btn-primary" href="https://poudreverte.org/" target="_blank" rel="noopener noreferrer">
|
||||
Ouvrir le site Reveleant
|
||||
<span aria-hidden="true">↗</span>
|
||||
</a>
|
||||
<a class="btn" href="#fonctionnalites">Voir les fonctionnalités</a>
|
||||
</div>
|
||||
<div class="quick-links" aria-label="Liens rapides composants Inventory">
|
||||
<div class="ql-row">
|
||||
<span class="ql-label">🧭 Rendus HTML</span>
|
||||
<a class="ql" href="/disk.html" title="Partitions">💽 Partitions</a>
|
||||
<a class="ql" href="/load.html" title="Charge">📈 Charge</a>
|
||||
<a class="ql" href="/mem.html" title="Mémoire">🧠 Mémoire</a>
|
||||
<a class="ql" href="/network.html" title="Carte réseau">🛰️ Réseau</a>
|
||||
<a class="ql" href="/procs.html" title="Processus">🧩 Processus</a>
|
||||
</div>
|
||||
|
||||
<div class="ql-row">
|
||||
<span class="ql-label">🧪 Rendus JSON</span>
|
||||
<a class="ql" href="/cpu" title="Processeurs">🧷 CPU</a>
|
||||
<a class="ql" href="/ps" title="Processus">🧩 PS</a>
|
||||
<a class="ql" href="/net" title="Carte réseau">🛰️ NET</a>
|
||||
<a class="ql" href="/mem" title="Mémoire">🧠 MEM</a>
|
||||
<a class="ql" href="/disk" title="Disque">💽 DISK</a>
|
||||
<a class="ql" href="/load" title="Charge">📈 LOAD</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel hero-right" aria-label="Visuel">
|
||||
<div class="logo-card">
|
||||
<div class="glow" aria-hidden="true"></div>
|
||||
<div class="scanline" aria-hidden="true"></div>
|
||||
|
||||
<!-- Remplacez inventory.png si vous souhaitez un autre nom de fichier -->
|
||||
<img class="logo-image logo-heartbeat" src="img/inventory.png" alt="Logo Inventory" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="fonctionnalites" class="features" aria-label="Fonctionnalités">
|
||||
<article class="card">
|
||||
<h3><span class="ico" aria-hidden="true">⟲</span>Découverte & inventaire</h3>
|
||||
<p>
|
||||
Collecte automatique des constantes systèmes (CPU, RAM, stockage, OS, logiciels, services),
|
||||
avec un format homogène pour simplifier le reporting.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="card" id="securite">
|
||||
<h3><span class="ico" aria-hidden="true">🛡</span>Sécurisé par conception</h3>
|
||||
<p>
|
||||
Communication chiffrée, authentification de l’agent, et principe du moindre privilège.
|
||||
L’inventaire sans ouvrir des portes inutiles.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3><span class="ico" aria-hidden="true">⚡</span>API & intégrations</h3>
|
||||
<p>
|
||||
Données prêtes pour l’ITSM / CMDB : endpoints simples, export, et intégration dans vos
|
||||
dashboards (et vos scripts qui sauvent la journée).
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<footer id="contact" aria-label="Pied de page">
|
||||
<div class="badge"><span class="mini" aria-hidden="true"></span>Inventory • Agent actif</div>
|
||||
|
||||
<div class="fine">
|
||||
Édité par <strong>Reveleant Software Solution</strong> —
|
||||
<a href="https://poudreverte.org/" target="_blank" rel="noopener noreferrer">accéder au site</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
196
inventory/src/www/load.html
Normal file
196
inventory/src/www/load.html
Normal file
@@ -0,0 +1,196 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Charge système — Progressbars</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin:0; padding:24px; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:canvas; color:canvastext; }
|
||||
.card { max-width:880px; margin:0 auto 16px; padding:16px 20px; border:1px solid color-mix(in oklab, canvastext 15%, transparent); border-radius:16px; background:color-mix(in oklab, canvas 92%, canvastext 0%); box-shadow:0 1px 8px color-mix(in oklab, canvastext 10%, transparent); }
|
||||
h1, h2 { margin:0 0 8px; font-size:1.2rem; }
|
||||
.muted { opacity:.75; font-size:.9rem; }
|
||||
|
||||
.row { display:grid; grid-template-columns: 140px 1fr 80px; gap:12px; align-items:center; margin:12px 0; }
|
||||
.label { font-weight:600; }
|
||||
|
||||
/* Progressbar */
|
||||
.bar {
|
||||
height:18px; width:100%; border-radius:12px; overflow:hidden;
|
||||
background: color-mix(in oklab, canvas 85%, canvastext 0%);
|
||||
outline: 1px solid color-mix(in oklab, canvastext 15%, transparent);
|
||||
position: relative;
|
||||
}
|
||||
.fill {
|
||||
height:100%; width:0%;
|
||||
background: linear-gradient(90deg, #37b24d, #1e90ff);
|
||||
transition: width .35s ease;
|
||||
}
|
||||
/* Couleur selon charge (vert → orange → rouge) via data-level */
|
||||
.fill[data-level="low"] { background: linear-gradient(90deg, #37b24d, #66d17a); }
|
||||
.fill[data-level="mid"] { background: linear-gradient(90deg, #f59f00, #ffd43b); }
|
||||
.fill[data-level="high"] { background: linear-gradient(90deg, #e03131, #ff6b6b); }
|
||||
|
||||
.val { text-align:right; font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- LOAD AVERAGE -->
|
||||
<section class="card">
|
||||
<h1>Charge système (load average)</h1>
|
||||
<div class="muted">Source : <code>/load</code> — jauges 0 → 4.0</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-live="polite">
|
||||
<div class="row">
|
||||
<div class="label">1 minute</div>
|
||||
<div class="bar"><div id="f1" class="fill" data-level="low"></div></div>
|
||||
<div class="val" id="v1">0.00</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">5 minutes</div>
|
||||
<div class="bar"><div id="f5" class="fill" data-level="low"></div></div>
|
||||
<div class="val" id="v5">0.00</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">15 minutes</div>
|
||||
<div class="bar"><div id="f15" class="fill" data-level="low"></div></div>
|
||||
<div class="val" id="v15">0.00</div>
|
||||
</div>
|
||||
<div class="muted" id="ts"></div>
|
||||
</section>
|
||||
|
||||
<!-- CPU PER-CORE LOAD -->
|
||||
<section class="card">
|
||||
<h2>Charge CPU par cœur</h2>
|
||||
<div class="muted">Source : <code>/cpu</code> — <code>usage_percent</code> 0 → 100 (par cœur)</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-live="polite" id="cpuCard">
|
||||
<div id="cpuRows">
|
||||
<!-- lignes CPU insérées dynamiquement -->
|
||||
</div>
|
||||
<div class="muted" id="tsCpu"></div>
|
||||
</section>
|
||||
|
||||
|
||||
<script>
|
||||
// --- Helpers ---
|
||||
function pickBase(templateValue, fallback) {
|
||||
let b = (templateValue || '').trim();
|
||||
// Si le fichier est servi sans templating, la variable ressemblera à "{{.ServerURL}}"
|
||||
if (!b || b.includes('{{')) b = fallback;
|
||||
return b.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function level(v, max) {
|
||||
const r = max > 0 ? (v / max) : 0;
|
||||
if (r < 0.5) return "low";
|
||||
if (r < 0.8) return "mid";
|
||||
return "high";
|
||||
}
|
||||
|
||||
// Bases / endpoints (avec fallback)
|
||||
const LOAD_BASE = pickBase("{{.LoadServerURL}}", "/");
|
||||
const CPU_BASE = pickBase("{{.CpuServerURL}}", "/");
|
||||
|
||||
// --- LOAD AVERAGE ---
|
||||
const LOAD_SCALE_MAX = 4.0;
|
||||
|
||||
async function loadAvg() {
|
||||
try {
|
||||
const res = await fetch(LOAD_BASE + "/load", { headers: { "Accept": "application/json" } });
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const d = await res.json(); // { avg: {load1,load5,load15}, misc: {...} }
|
||||
|
||||
const avg = (d && d.avg) ? d.avg : {};
|
||||
const l1 = +avg.load1 || 0, l5 = +avg.load5 || 0, l15 = +avg.load15 || 0;
|
||||
|
||||
const w1 = Math.max(0, Math.min(100, (l1 / LOAD_SCALE_MAX) * 100));
|
||||
const w5 = Math.max(0, Math.min(100, (l5 / LOAD_SCALE_MAX) * 100));
|
||||
const w15 = Math.max(0, Math.min(100, (l15 / LOAD_SCALE_MAX) * 100));
|
||||
|
||||
const f1 = document.getElementById("f1");
|
||||
const f5 = document.getElementById("f5");
|
||||
const f15 = document.getElementById("f15");
|
||||
|
||||
f1.style.width = w1 + "%"; f1.setAttribute("data-level", level(l1, LOAD_SCALE_MAX));
|
||||
f5.style.width = w5 + "%"; f5.setAttribute("data-level", level(l5, LOAD_SCALE_MAX));
|
||||
f15.style.width = w15 + "%"; f15.setAttribute("data-level", level(l15, LOAD_SCALE_MAX));
|
||||
|
||||
document.getElementById("v1").textContent = l1.toFixed(2);
|
||||
document.getElementById("v5").textContent = l5.toFixed(2);
|
||||
document.getElementById("v15").textContent = l15.toFixed(2);
|
||||
document.getElementById("ts").textContent = "Mis à jour : " + new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
["f1","f5","f15"].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.style.width = "0%";
|
||||
el.setAttribute("data-level", "low");
|
||||
});
|
||||
document.getElementById("ts").textContent = "Erreur /load : " + new Date().toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CPU PER-CORE (usage %) ---
|
||||
const CPU_SCALE_MAX = 100.0; // usage_percent 0 → 100
|
||||
|
||||
function ensureCpuBars(n) {
|
||||
const wrap = document.getElementById("cpuRows");
|
||||
if (wrap.dataset.count === String(n)) return;
|
||||
|
||||
let html = "";
|
||||
for (let i = 0; i < n; i++) {
|
||||
html += '<div class="row">'
|
||||
+ '<div class="label">CPU ' + i + '</div>'
|
||||
+ '<div class="bar"><div id="fc-' + i + '" class="fill" data-level="low"></div></div>'
|
||||
+ '<div class="val" id="vc-' + i + '">0.00%</div>'
|
||||
+ "</div>";
|
||||
}
|
||||
wrap.innerHTML = html || '<div class="muted">Aucun cœur détecté</div>';
|
||||
wrap.dataset.count = String(n);
|
||||
}
|
||||
|
||||
async function loadCpu() {
|
||||
try {
|
||||
const res = await fetch(CPU_BASE + "/cpu", { headers: { "Accept": "application/json" } });
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const d = await res.json(); // { cores:[{usage_percent:... , info:{cpu:...}}] }
|
||||
|
||||
const cores = (d && Array.isArray(d.cores)) ? d.cores : [];
|
||||
const n = cores.length;
|
||||
ensureCpuBars(n);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const v = +cores[i].usage_percent || 0;
|
||||
const w = Math.max(0, Math.min(100, (v / CPU_SCALE_MAX) * 100));
|
||||
const f = document.getElementById("fc-" + i);
|
||||
const t = document.getElementById("vc-" + i);
|
||||
if (f) { f.style.width = w + "%"; f.setAttribute("data-level", level(v, CPU_SCALE_MAX)); }
|
||||
if (t) { t.textContent = v.toFixed(2) + "%"; }
|
||||
}
|
||||
document.getElementById("tsCpu").textContent = "Mis à jour : " + new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
const wrap = document.getElementById("cpuRows");
|
||||
const n = +(wrap.dataset.count || 0);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const f = document.getElementById("fc-" + i);
|
||||
const t = document.getElementById("vc-" + i);
|
||||
if (f) { f.style.width = "0%"; f.setAttribute("data-level","low"); }
|
||||
if (t) { t.textContent = "0.00%"; }
|
||||
}
|
||||
document.getElementById("tsCpu").textContent = "Erreur /cpu : " + new Date().toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
// Première charge + rafraîchissement
|
||||
loadAvg();
|
||||
loadCpu();
|
||||
setInterval(() => { loadAvg(); loadCpu(); }, 5000);
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
173
inventory/src/www/mem.html
Normal file
173
inventory/src/www/mem.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mémoire système</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin:0; padding:24px; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:canvas; color:canvastext; }
|
||||
.card { max-width:1000px; margin:0 auto 16px; padding:16px 20px; border:1px solid color-mix(in oklab, canvastext 15%, transparent); border-radius:16px; background:color-mix(in oklab, canvas 92%, canvastext 0%); box-shadow:0 1px 8px color-mix(in oklab, canvastext 10%, transparent); }
|
||||
h1 { margin:0 0 8px; font-size:1.25rem; }
|
||||
.muted { opacity:.75; font-size:.9rem; }
|
||||
.row { display:grid; grid-template-columns: 160px 1fr 110px; gap:12px; align-items:center; margin:12px 0; }
|
||||
.label { font-weight:600; }
|
||||
.val { text-align:right; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* Progressbar */
|
||||
.bar { height:18px; width:100%; border-radius:12px; overflow:hidden;
|
||||
background: color-mix(in oklab, canvas 85%, canvastext 0%);
|
||||
outline: 1px solid color-mix(in oklab, canvastext 15%, transparent); position:relative; }
|
||||
.fill { height:100%; width:0%; transition:width .35s ease; }
|
||||
.fill.ram { background: linear-gradient(90deg, #37b24d, #1e90ff); }
|
||||
.fill.swap { background: linear-gradient(90deg, #f59f00, #ff6b6b); }
|
||||
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:12px; }
|
||||
.kv { padding:10px 12px; border:1px solid color-mix(in oklab, canvastext 15%, transparent); border-radius:12px; background:color-mix(in oklab, canvas 96%, canvastext 0%); }
|
||||
.k { font-size:.85rem; opacity:.8; }
|
||||
.v { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||
|
||||
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||||
.btn { padding:6px 10px; border-radius:10px; border:1px solid color-mix(in oklab, dodgerblue 40%, transparent); background: color-mix(in oklab, dodgerblue 35%, canvas); color:white; cursor:pointer; }
|
||||
.btn[disabled]{ opacity:.6; cursor:default; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="card">
|
||||
<h1>Mémoire système</h1>
|
||||
<div class="muted">Source : <code>/mem</code> — rafraîchissement toutes les 5s</div>
|
||||
<div class="toolbar" style="margin-top:8px">
|
||||
<button id="refresh" class="btn" type="button">Rafraîchir</button>
|
||||
<span id="ts" class="muted"></span>
|
||||
<span id="status" class="muted"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-live="polite">
|
||||
<div class="row">
|
||||
<div class="label">RAM utilisée</div>
|
||||
<div class="bar"><div id="ramFill" class="fill ram"></div></div>
|
||||
<div class="val" id="ramPct">0.00%</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">SWAP utilisée</div>
|
||||
<div class="bar"><div id="swapFill" class="fill swap"></div></div>
|
||||
<div class="val" id="swapPct">0.00%</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="grid" id="summary">
|
||||
<!-- Rempli dynamiquement -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const MEM_URL = '/mem';
|
||||
const REFRESH_INTERVAL = 5000; // 5s
|
||||
let refreshing = false;
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
function fmtBytes(n) {
|
||||
n = Number(n)||0;
|
||||
const units = ['B','KiB','MiB','GiB','TiB','PiB'];
|
||||
let i = 0;
|
||||
while (n >= 1024 && i < units.length-1) { n /= 1024; i++; }
|
||||
const s = (n < 10 ? n.toFixed(2) : n < 100 ? n.toFixed(1) : n.toFixed(0));
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
function setBar(el, pct) {
|
||||
const p = Math.max(0, Math.min(100, Number(pct)||0));
|
||||
el.style.width = p.toFixed(2) + '%';
|
||||
}
|
||||
function kv(key, val) {
|
||||
return '<div class="kv"><div class="k">'+esc(key)+'</div><div class="v">'+esc(val)+'</div></div>';
|
||||
}
|
||||
|
||||
async function loadMem() {
|
||||
if (refreshing) return; // évite l’empilement si l’API est lente
|
||||
refreshing = true;
|
||||
|
||||
const ts = document.getElementById('ts');
|
||||
const status = document.getElementById('status');
|
||||
const ramFill = document.getElementById('ramFill');
|
||||
const swapFill = document.getElementById('swapFill');
|
||||
const ramPct = document.getElementById('ramPct');
|
||||
const swapPct = document.getElementById('swapPct');
|
||||
const summary = document.getElementById('summary');
|
||||
|
||||
status.textContent = '⏳ mise à jour…';
|
||||
|
||||
try {
|
||||
const res = await fetch(MEM_URL, { headers: { 'Accept': 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
|
||||
const m = await res.json();
|
||||
const v = m.virtual || {};
|
||||
const s = m.swap || {};
|
||||
|
||||
// RAM
|
||||
const ramUsedPct = Number(v.usedPercent) || 0;
|
||||
setBar(ramFill, ramUsedPct);
|
||||
ramPct.textContent = ramUsedPct.toFixed(2) + '%';
|
||||
|
||||
// SWAP
|
||||
const swapUsedPct = Number(s.usedPercent) || 0;
|
||||
setBar(swapFill, swapUsedPct);
|
||||
swapPct.textContent = swapUsedPct.toFixed(2) + '%';
|
||||
|
||||
// Résumé
|
||||
const items = [
|
||||
['RAM totale', fmtBytes(v.total)],
|
||||
['RAM utilisée', fmtBytes(v.used)],
|
||||
['RAM libre', fmtBytes(v.free)],
|
||||
['RAM disponible', fmtBytes(v.available)],
|
||||
['Active', fmtBytes(v.active)],
|
||||
['Inactive', fmtBytes(v.inactive)],
|
||||
['Buffers', fmtBytes(v.buffers)],
|
||||
['Cached', fmtBytes(v.cached)],
|
||||
['Slab', fmtBytes(v.slab)],
|
||||
['SReclaimable', fmtBytes(v.sreclaimable)],
|
||||
['SUnreclaim', fmtBytes(v.sunreclaim)],
|
||||
['Commit Limit', fmtBytes(v.commitLimit)],
|
||||
['Committed AS', fmtBytes(v.committedAS)],
|
||||
['Swap total', fmtBytes(s.total)],
|
||||
['Swap utilisée', fmtBytes(s.used)],
|
||||
['Swap libre', fmtBytes(s.free)],
|
||||
['Page In', String(Number(s.pgIn)||0)],
|
||||
['Page Out', String(Number(s.pgOut)||0)]
|
||||
];
|
||||
|
||||
summary.innerHTML = items.map(([k,val]) => kv(k, val)).join('');
|
||||
ts.textContent = 'Mis à jour : ' + new Date().toLocaleTimeString();
|
||||
status.textContent = '✅ OK';
|
||||
|
||||
} catch (e) {
|
||||
summary.innerHTML = '<div class="kv"><div class="k">Erreur</div><div class="v">' + esc(e.message) + '</div></div>';
|
||||
status.textContent = '❌ ' + e.message;
|
||||
} finally {
|
||||
refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Bouton
|
||||
document.getElementById('refresh').addEventListener('click', async function(){
|
||||
const btn = this, prev = btn.textContent;
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try { await loadMem(); }
|
||||
finally { btn.disabled = false; btn.textContent = prev; }
|
||||
});
|
||||
|
||||
// Chargement immédiat + refresh 5s
|
||||
loadMem();
|
||||
setInterval(loadMem, REFRESH_INTERVAL);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
277
inventory/src/www/network.html
Normal file
277
inventory/src/www/network.html
Normal file
@@ -0,0 +1,277 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Réseau — interfaces & débits</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin:0; padding:24px; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:canvas; color:canvastext; }
|
||||
.card { max-width:1200px; margin:0 auto 16px; padding:16px 20px;
|
||||
border:1px solid color-mix(in oklab, canvastext 15%, transparent);
|
||||
border-radius:16px;
|
||||
background:color-mix(in oklab, canvas 92%, canvastext 0%);
|
||||
box-shadow:0 1px 8px color-mix(in oklab, canvastext 10%, transparent); }
|
||||
h1 { margin:0 0 8px; font-size:1.25rem; }
|
||||
.muted { opacity:.75; font-size:.9rem; }
|
||||
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:8px; }
|
||||
.btn { padding:6px 10px; border-radius:10px; border:1px solid color-mix(in oklab, dodgerblue 40%, transparent);
|
||||
background: color-mix(in oklab, dodgerblue 35%, canvas); color:white; cursor:pointer; }
|
||||
.btn[disabled]{ opacity:.6; cursor:default; }
|
||||
.pill { display:inline-block; padding:.1rem .45rem; border-radius:.75rem;
|
||||
border:1px solid color-mix(in oklab, canvastext 25%, transparent); font-size:.85rem; }
|
||||
input[type="checkbox"]{ transform: translateY(1px); }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
th, td { padding:8px 10px; border-bottom:1px solid color-mix(in oklab, canvastext 12%, transparent); text-align:left; vertical-align:top; }
|
||||
th { font-weight:600; user-select:none; cursor:pointer; }
|
||||
.right { text-align:right; }
|
||||
.mono { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size:.9rem; font-variant-numeric: tabular-nums; }
|
||||
.addr { display:inline-block; margin:2px 0; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||
.small { font-size:.9rem; opacity:.9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="card">
|
||||
<h1>Interfaces réseau</h1>
|
||||
<div class="muted">Source : <code>/net</code> — calcul des débits via delta <span class="mono">bytes</span> — rafraîchissement toutes les 5s</div>
|
||||
<div class="toolbar">
|
||||
<label class="small"><input id="inclLo" type="checkbox"> inclure loopback</label>
|
||||
<label class="small"><input id="onlyUp" type="checkbox" checked> seulement interfaces "up"</label>
|
||||
<span id="count" class="pill">0 interfaces</span>
|
||||
<button id="refresh" class="btn" type="button">Rafraîchir</button>
|
||||
<span id="ts" class="muted"></span>
|
||||
<span id="statusText" class="muted"></span>
|
||||
</div>
|
||||
<div class="muted small" style="margin-top:8px">
|
||||
Tri: clique sur <span class="mono">Rx</span> / <span class="mono">Tx</span> / <span class="mono">Interface</span>.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="name">Interface</th>
|
||||
<th class="noclick">Adresses</th>
|
||||
<th class="right" data-sort="rx">Rx (Mb/s)</th>
|
||||
<th class="right" data-sort="tx">Tx (Mb/s)</th>
|
||||
<th class="right" data-sort="rxB">Rx total</th>
|
||||
<th class="right" data-sort="txB">Tx total</th>
|
||||
<th class="right" data-sort="mtu">MTU</th>
|
||||
<th class="noclick">Flags / MAC</th>
|
||||
<th class="right" data-sort="drops">Drops</th>
|
||||
<th class="right" data-sort="errs">Errors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
<tr><td colspan="10" class="muted">Chargement…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const NET_URL = '/net';
|
||||
const REFRESH_INTERVAL = 5000; // 5s
|
||||
let refreshing = false;
|
||||
|
||||
// For rate computation
|
||||
const prev = new Map(); // name -> {t, rx, tx}
|
||||
|
||||
// Sorting
|
||||
let sortKey = 'rx';
|
||||
let sortDir = 'desc';
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
function num(x){ x = Number(x); return Number.isFinite(x) ? x : 0; }
|
||||
|
||||
function fmtBytes(n) {
|
||||
n = Number(n)||0;
|
||||
const units = ['B','KiB','MiB','GiB','TiB','PiB'];
|
||||
let i = 0;
|
||||
while (n >= 1024 && i < units.length-1) { n /= 1024; i++; }
|
||||
const s = (n < 10 ? n.toFixed(2) : n < 100 ? n.toFixed(1) : n.toFixed(0));
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function computeRates(items) {
|
||||
const now = Date.now();
|
||||
items.forEach(it => {
|
||||
const name = (it.interface && it.interface.name) || (it.io && it.io.name) || '';
|
||||
const io = it.io || {};
|
||||
const rx = num(io.bytesRecv);
|
||||
const tx = num(io.bytesSent);
|
||||
|
||||
const p = prev.get(name);
|
||||
if (!p) {
|
||||
prev.set(name, { t: now, rx, tx, rx_mbps: 0, tx_mbps: 0 });
|
||||
it._rx_mbps = 0; it._tx_mbps = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = Math.max(0.001, (now - p.t) / 1000); // seconds
|
||||
const drx = Math.max(0, rx - p.rx);
|
||||
const dtx = Math.max(0, tx - p.tx);
|
||||
|
||||
// bytes/s -> Mb/s (decimal megabit)
|
||||
const rx_mbps = (drx * 8) / dt / 1_000_000;
|
||||
const tx_mbps = (dtx * 8) / dt / 1_000_000;
|
||||
|
||||
prev.set(name, { t: now, rx, tx, rx_mbps, tx_mbps });
|
||||
|
||||
it._rx_mbps = rx_mbps;
|
||||
it._tx_mbps = tx_mbps;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function applyFilters(items) {
|
||||
const inclLo = document.getElementById('inclLo').checked;
|
||||
const onlyUp = document.getElementById('onlyUp').checked;
|
||||
|
||||
return (items || []).filter(it => {
|
||||
const inf = it.interface || {};
|
||||
const name = String(inf.name || '');
|
||||
const flags = Array.isArray(inf.flags) ? inf.flags : [];
|
||||
|
||||
if (!inclLo && name === 'lo') return false;
|
||||
if (onlyUp && !flags.includes('up')) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function comparator(a, b) {
|
||||
const dir = (sortDir === 'asc') ? 1 : -1;
|
||||
|
||||
const ai = a.interface || {};
|
||||
const bi = b.interface || {};
|
||||
const aio = a.io || {};
|
||||
const bio = b.io || {};
|
||||
|
||||
if (sortKey === 'name') return dir * String(ai.name||'').localeCompare(String(bi.name||''), 'fr', {sensitivity:'base'});
|
||||
if (sortKey === 'rx') return dir * (num(a._rx_mbps) - num(b._rx_mbps));
|
||||
if (sortKey === 'tx') return dir * (num(a._tx_mbps) - num(b._tx_mbps));
|
||||
if (sortKey === 'rxB') return dir * (num(aio.bytesRecv) - num(bio.bytesRecv));
|
||||
if (sortKey === 'txB') return dir * (num(aio.bytesSent) - num(bio.bytesSent));
|
||||
if (sortKey === 'mtu') return dir * (num(ai.mtu) - num(bi.mtu));
|
||||
if (sortKey === 'drops') return dir * ((num(aio.dropin)+num(aio.dropout)) - (num(bio.dropin)+num(bio.dropout)));
|
||||
if (sortKey === 'errs') return dir * ((num(aio.errin)+num(aio.errout)) - (num(bio.errin)+num(bio.errout)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
const tbody = document.getElementById('tbody');
|
||||
const count = document.getElementById('count');
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="muted">Aucune interface</td></tr>';
|
||||
count.textContent = '0 interfaces';
|
||||
return;
|
||||
}
|
||||
|
||||
count.textContent = items.length + ' interfaces';
|
||||
|
||||
tbody.innerHTML = items.map(it => {
|
||||
const inf = it.interface || {};
|
||||
const io = it.io || {};
|
||||
const name = inf.name || io.name || '';
|
||||
const mtu = inf.mtu ?? '';
|
||||
const mac = inf.hardwareAddr || '';
|
||||
const flags = Array.isArray(inf.flags) ? inf.flags : [];
|
||||
const addrs = Array.isArray(inf.addrs) ? inf.addrs : [];
|
||||
const addrHtml = addrs.map(a => '<div class="mono addr">' + esc(a.addr || '') + '</div>').join('') || '<span class="muted">—</span>';
|
||||
|
||||
const rx_mbps = num(it._rx_mbps);
|
||||
const tx_mbps = num(it._tx_mbps);
|
||||
|
||||
const dropIn = num(io.dropin), dropOut = num(io.dropout);
|
||||
const errIn = num(io.errin), errOut = num(io.errout);
|
||||
|
||||
const flagsHtml = flags.map(f => '<span class="pill">' + esc(f) + '</span>').join(' ') || '<span class="muted">—</span>';
|
||||
|
||||
return '<tr>'
|
||||
+ '<td><span class="pill mono">' + esc(name) + '</span></td>'
|
||||
+ '<td>' + addrHtml + '</td>'
|
||||
+ '<td class="right mono">' + rx_mbps.toFixed(3) + '</td>'
|
||||
+ '<td class="right mono">' + tx_mbps.toFixed(3) + '</td>'
|
||||
+ '<td class="right mono">' + esc(fmtBytes(io.bytesRecv)) + '</td>'
|
||||
+ '<td class="right mono">' + esc(fmtBytes(io.bytesSent)) + '</td>'
|
||||
+ '<td class="right mono">' + esc(mtu) + '</td>'
|
||||
+ '<td class="mono">' + flagsHtml + (mac ? ('<div class="muted mono" style="margin-top:4px">' + esc(mac) + '</div>') : '') + '</td>'
|
||||
+ '<td class="right mono">' + (dropIn + dropOut) + '</td>'
|
||||
+ '<td class="right mono">' + (errIn + errOut) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let DATA = [];
|
||||
|
||||
async function loadNet() {
|
||||
if (refreshing) return;
|
||||
refreshing = true;
|
||||
|
||||
const statusText = document.getElementById('statusText');
|
||||
statusText.textContent = '⏳ mise à jour…';
|
||||
|
||||
try {
|
||||
const res = await fetch(NET_URL, { headers: { 'Accept': 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const arr = await res.json();
|
||||
DATA = Array.isArray(arr) ? arr : [];
|
||||
|
||||
computeRates(DATA);
|
||||
|
||||
let view = applyFilters(DATA).slice().sort(comparator);
|
||||
render(view);
|
||||
|
||||
document.getElementById('ts').textContent = 'Mis à jour : ' + new Date().toLocaleTimeString();
|
||||
statusText.textContent = '✅ OK';
|
||||
} catch (e) {
|
||||
document.getElementById('tbody').innerHTML =
|
||||
'<tr><td colspan="10" class="muted">Erreur: ' + esc(e.message) + '</td></tr>';
|
||||
document.getElementById('count').textContent = '0 interfaces';
|
||||
statusText.textContent = '❌ ' + e.message;
|
||||
} finally {
|
||||
refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// UI events
|
||||
document.getElementById('inclLo').addEventListener('change', () => render(applyFilters(DATA).slice().sort(comparator)));
|
||||
document.getElementById('onlyUp').addEventListener('change', () => render(applyFilters(DATA).slice().sort(comparator)));
|
||||
|
||||
document.getElementById('refresh').addEventListener('click', async function(){
|
||||
const btn = this, prev = btn.textContent;
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try { await loadNet(); }
|
||||
finally { btn.disabled = false; btn.textContent = prev; }
|
||||
});
|
||||
|
||||
// Click headers to toggle sorting
|
||||
document.querySelectorAll('th[data-sort]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const k = th.getAttribute('data-sort');
|
||||
if (!k) return;
|
||||
if (sortKey === k) sortDir = (sortDir === 'asc') ? 'desc' : 'asc';
|
||||
else {
|
||||
sortKey = k;
|
||||
sortDir = (k === 'name' || k === 'mtu') ? 'asc' : 'desc';
|
||||
}
|
||||
render(applyFilters(DATA).slice().sort(comparator));
|
||||
});
|
||||
});
|
||||
|
||||
// initial + 5s refresh
|
||||
loadNet();
|
||||
setInterval(loadNet, REFRESH_INTERVAL);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
236
inventory/src/www/procs.html
Normal file
236
inventory/src/www/procs.html
Normal file
@@ -0,0 +1,236 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Processus système</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin:0; padding:24px; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:canvas; color:canvastext; }
|
||||
.card { max-width:1200px; margin:0 auto 16px; padding:16px 20px; border:1px solid color-mix(in oklab, canvastext 15%, transparent); border-radius:16px; background:color-mix(in oklab, canvas 92%, canvastext 0%); box-shadow:0 1px 8px color-mix(in oklab, canvastext 10%, transparent); }
|
||||
h1 { margin:0 0 8px; font-size:1.25rem; }
|
||||
.muted { opacity:.75; font-size:.9rem; }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
th, td { padding:8px 10px; border-bottom:1px solid color-mix(in oklab, canvastext 12%, transparent); text-align:left; vertical-align:top; }
|
||||
th { font-weight:600; user-select:none; cursor:pointer; }
|
||||
th.noclick { cursor:default; }
|
||||
.right { text-align:right; }
|
||||
.mono { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size:.9rem; font-variant-numeric: tabular-nums; }
|
||||
.nowrap { white-space:nowrap; }
|
||||
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||||
input[type="text"], select { padding:6px 8px; border-radius:10px; border:1px solid color-mix(in oklab, canvastext 25%, transparent); background: color-mix(in oklab, canvas 95%, canvastext 0%); color:inherit; }
|
||||
.pill { display:inline-block; padding:.1rem .45rem; border-radius:.75rem; border:1px solid color-mix(in oklab, canvastext 25%, transparent); font-size:.85rem; }
|
||||
.btn { padding:6px 10px; border-radius:10px; border:1px solid color-mix(in oklab, dodgerblue 40%, transparent); background: color-mix(in oklab, dodgerblue 35%, canvas); color:white; cursor:pointer; }
|
||||
.btn[disabled]{ opacity:.6; cursor:default; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||
.hint { font-size:.85rem; opacity:.75; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="card">
|
||||
<h1>Processus système</h1>
|
||||
<div class="muted">Source : <code>/ps</code> — rafraîchissement toutes les 5s</div>
|
||||
<div class="toolbar" style="margin-top:8px">
|
||||
<label>Filtrer : <input id="q" type="text" placeholder="nom, user, pid…"></label>
|
||||
<label>Statut :
|
||||
<select id="status">
|
||||
<option value="">(tous)</option>
|
||||
<option value="running">running</option>
|
||||
<option value="sleep">sleep</option>
|
||||
<option value="idle">idle</option>
|
||||
<option value="blocked">blocked</option>
|
||||
<option value="stopped">stopped</option>
|
||||
<option value="zombie">zombie</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Trier :
|
||||
<select id="sort">
|
||||
<option value="cpu_desc">CPU ↓</option>
|
||||
<option value="mem_desc">Mémoire ↓</option>
|
||||
<option value="pid_asc">PID ↑</option>
|
||||
<option value="name_asc">Nom ↑</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="refresh" class="btn" type="button">Rafraîchir</button>
|
||||
<span id="count" class="pill">0 éléments</span>
|
||||
<span id="ts" class="muted"></span>
|
||||
<span id="statusText" class="muted"></span>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:8px">Astuce : clique sur les en-têtes CPU/Mémoire/PID/Nom pour changer le tri.</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="pid" class="right">PID</th>
|
||||
<th data-sort="name">Nom</th>
|
||||
<th data-sort="user">Utilisateur</th>
|
||||
<th data-sort="status">Statut</th>
|
||||
<th data-sort="cpu" class="right">CPU %</th>
|
||||
<th data-sort="memory" class="right">Mémoire %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
<tr><td colspan="6" class="muted">Chargement…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const PS_URL = '/ps';
|
||||
const REFRESH_INTERVAL = 5000; // 5s
|
||||
let refreshing = false;
|
||||
|
||||
// tri state
|
||||
let sortKey = 'cpu';
|
||||
let sortDir = 'desc'; // 'asc'|'desc'
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"')
|
||||
.replace(/'/g,''');
|
||||
}
|
||||
function num(x) { x = Number(x); return Number.isFinite(x) ? x : 0; }
|
||||
|
||||
function applySortSelection() {
|
||||
const v = document.getElementById('sort').value;
|
||||
if (v === 'cpu_desc') { sortKey='cpu'; sortDir='desc'; }
|
||||
else if (v === 'mem_desc') { sortKey='memory'; sortDir='desc'; }
|
||||
else if (v === 'pid_asc') { sortKey='pid'; sortDir='asc'; }
|
||||
else if (v === 'name_asc') { sortKey='name'; sortDir='asc'; }
|
||||
}
|
||||
|
||||
function comparator(a, b) {
|
||||
const dir = (sortDir === 'asc') ? 1 : -1;
|
||||
if (sortKey === 'pid') return dir * (num(a.pid) - num(b.pid));
|
||||
if (sortKey === 'cpu') return dir * (num(a.cpu) - num(b.cpu));
|
||||
if (sortKey === 'memory') return dir * (num(a.memory) - num(b.memory));
|
||||
if (sortKey === 'name') return dir * String(a.name||'').localeCompare(String(b.name||''), 'fr', {sensitivity:'base'});
|
||||
if (sortKey === 'user') return dir * String(a.user||'').localeCompare(String(b.user||''), 'fr', {sensitivity:'base'});
|
||||
if (sortKey === 'status') return dir * String(a.status||'').localeCompare(String(b.status||''), 'fr', {sensitivity:'base'});
|
||||
return 0;
|
||||
}
|
||||
|
||||
function filterItems(items) {
|
||||
const q = (document.getElementById('q').value || '').trim().toLowerCase();
|
||||
const st = document.getElementById('status').value;
|
||||
|
||||
return (items || []).filter(p => {
|
||||
const pid = String(p.pid ?? '');
|
||||
const name = String(p.name ?? '');
|
||||
const user = String(p.user ?? '');
|
||||
const status = String(p.status ?? '');
|
||||
const hay = (pid + ' ' + name + ' ' + user).toLowerCase();
|
||||
if (q && !hay.includes(q)) return false;
|
||||
if (st && status !== st) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
const tbody = document.getElementById('tbody');
|
||||
const count = document.getElementById('count');
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">Aucune donnée</td></tr>';
|
||||
count.textContent = '0 éléments';
|
||||
return;
|
||||
}
|
||||
|
||||
count.textContent = items.length + ' éléments';
|
||||
|
||||
tbody.innerHTML = items.map(p => {
|
||||
const pid = p.pid ?? '';
|
||||
const name = p.name ?? '';
|
||||
const user = p.user ?? '';
|
||||
const status = p.status ?? '';
|
||||
const cpu = num(p.cpu);
|
||||
const mem = num(p.memory);
|
||||
return (
|
||||
'<tr>'
|
||||
+ '<td class="right mono">' + esc(pid) + '</td>'
|
||||
+ '<td class="mono">' + esc(name) + '</td>'
|
||||
+ '<td>' + esc(user) + '</td>'
|
||||
+ '<td><span class="pill">' + esc(status) + '</span></td>'
|
||||
+ '<td class="right mono">' + cpu.toFixed(3) + '</td>'
|
||||
+ '<td class="right mono">' + mem.toFixed(3) + '</td>'
|
||||
+ '</tr>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let DATA = [];
|
||||
|
||||
async function loadPs() {
|
||||
if (refreshing) return;
|
||||
refreshing = true;
|
||||
|
||||
const statusText = document.getElementById('statusText');
|
||||
statusText.textContent = '⏳ mise à jour…';
|
||||
|
||||
try {
|
||||
const res = await fetch(PS_URL, { headers: { 'Accept': 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const arr = await res.json();
|
||||
DATA = Array.isArray(arr) ? arr : [];
|
||||
applyFiltersAndRender();
|
||||
document.getElementById('ts').textContent = 'Mis à jour : ' + new Date().toLocaleTimeString();
|
||||
statusText.textContent = '✅ OK';
|
||||
} catch (e) {
|
||||
document.getElementById('tbody').innerHTML =
|
||||
'<tr><td colspan="6" class="muted">Erreur: ' + esc(e.message) + '</td></tr>';
|
||||
document.getElementById('count').textContent = '0 éléments';
|
||||
statusText.textContent = '❌ ' + e.message;
|
||||
} finally {
|
||||
refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyFiltersAndRender() {
|
||||
applySortSelection();
|
||||
const filtered = filterItems(DATA).slice().sort(comparator);
|
||||
render(filtered);
|
||||
}
|
||||
|
||||
// UI events
|
||||
document.getElementById('q').addEventListener('input', applyFiltersAndRender);
|
||||
document.getElementById('status').addEventListener('change', applyFiltersAndRender);
|
||||
document.getElementById('sort').addEventListener('change', applyFiltersAndRender);
|
||||
|
||||
document.getElementById('refresh').addEventListener('click', async function(){
|
||||
const btn = this, prev = btn.textContent;
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try { await loadPs(); }
|
||||
finally { btn.disabled = false; btn.textContent = prev; }
|
||||
});
|
||||
|
||||
// Click headers to toggle sorting
|
||||
document.querySelectorAll('th[data-sort]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const k = th.getAttribute('data-sort');
|
||||
if (!k) return;
|
||||
if (sortKey === k) sortDir = (sortDir === 'asc') ? 'desc' : 'asc';
|
||||
else { sortKey = k; sortDir = (k === 'pid' || k === 'name' || k === 'user' || k === 'status') ? 'asc' : 'desc'; }
|
||||
|
||||
// reflect in dropdown best-effort
|
||||
const dd = document.getElementById('sort');
|
||||
if (sortKey === 'cpu' && sortDir === 'desc') dd.value = 'cpu_desc';
|
||||
else if (sortKey === 'memory' && sortDir === 'desc') dd.value = 'mem_desc';
|
||||
else if (sortKey === 'pid' && sortDir === 'asc') dd.value = 'pid_asc';
|
||||
else if (sortKey === 'name' && sortDir === 'asc') dd.value = 'name_asc';
|
||||
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
});
|
||||
|
||||
// initial + 5s refresh
|
||||
loadPs();
|
||||
setInterval(loadPs, REFRESH_INTERVAL);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user