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