premier commit

This commit is contained in:
MatBureau
2026-04-01 14:05:17 +02:00
commit 784838fe5c
20 changed files with 1987 additions and 0 deletions

22
inventory/Dockerfile Normal file
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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)
}

View 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
View File

@@ -0,0 +1,12 @@
package main
import (
"log"
"net/http"
)
func main() {
log.Println("listening on :80")
log.Fatal(http.ListenAndServe(":80", router()))
}

View 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
}

View 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
}

View 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
View 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
View 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,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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 linventaire 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 lagent, et principe du moindre privilège.
Linventaire 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 lITSM / 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
View 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
View 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,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
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 lempilement si lAPI 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>

View 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,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
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>

View 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,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
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>