Compare commits
10
Commits
v0.1.2
...
3933301cff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3933301cff | ||
|
|
9f87c9294d | ||
|
|
638d347bb0 | ||
|
|
8f3dbd0532 | ||
|
|
99bdf79a63 | ||
|
|
a22d1f4cd2 | ||
|
|
d8f395cb53 | ||
|
|
f69c22039b | ||
|
|
2bda420728 | ||
|
|
f604e22f6e |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -73,12 +73,12 @@
|
||||
<img id="pop-icon-img" src="" alt="" style="display:none">
|
||||
<div class="agent-icon-overlay"><i class="fa-solid fa-camera"></i><span>Changer</span></div>
|
||||
</div>
|
||||
<input type="file" id="icon-upload" accept=".svg,.jpg,.jpeg,.png,.webp" style="display:none">
|
||||
<input type="file" id="icon-upload" accept=".jpg,.jpeg,.png,.webp" style="display:none">
|
||||
<div style="flex:1">
|
||||
<div class="pop-host" id="pop-host">—</div>
|
||||
<div class="pop-ip" id="pop-ip">—</div>
|
||||
<div style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px">
|
||||
Cliquer sur l'icône pour personnaliser · SVG JPG PNG WEBP · max 128×128 px
|
||||
<div id="icon-hint" style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px">
|
||||
Cliquer sur l'icône pour personnaliser · JPG PNG WEBP · max 128×128 px
|
||||
</div>
|
||||
</div>
|
||||
<div class="pop-led" id="pop-led"></div>
|
||||
|
||||
@@ -58,7 +58,7 @@ const Grid = (() => {
|
||||
const iconContent = `<img src="${API.iconUrl(id)}" alt=""
|
||||
style="width:100%;height:100%;object-fit:cover;border-radius:7px"
|
||||
onerror="this.style.display='none';this.nextSibling.style.display='flex'">
|
||||
<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
|
||||
<span style="display:none;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
|
||||
<i class="fa-solid fa-server"></i></span>`;
|
||||
|
||||
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')">
|
||||
@@ -87,7 +87,7 @@ const Grid = (() => {
|
||||
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
|
||||
<div class="g-bar"><div class="g-fill ${offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : '')}"
|
||||
style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div>
|
||||
<span class="g-val">${offline ? '—' : fmtPct(diskPct)}</span>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
@@ -124,7 +124,7 @@ const Grid = (() => {
|
||||
function refresh(agents) {
|
||||
agents.forEach(a => {
|
||||
if (!_agents.has(a.id)) {
|
||||
_agents.set(a.id, { agent: a, metrics: null });
|
||||
_agents.set(a.id, { agent: a, metrics: a.last_metrics || null });
|
||||
} else {
|
||||
_agents.get(a.id).agent = a;
|
||||
}
|
||||
|
||||
+21
-2
@@ -30,8 +30,27 @@ const Popups = (() => {
|
||||
document.getElementById('icon-upload').onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
await API.uploadIcon(agentId, file);
|
||||
img.src = API.iconUrl(agentId) + '?t=' + Date.now();
|
||||
const hint = document.getElementById('icon-hint');
|
||||
try {
|
||||
await API.uploadIcon(agentId, file);
|
||||
const ts = '?t=' + Date.now();
|
||||
img.src = API.iconUrl(agentId) + ts;
|
||||
img.style.display = 'block';
|
||||
document.getElementById('pop-icon-fa').style.display = 'none';
|
||||
const tileImg = document.querySelector(`#tile-${CSS.escape(agentId)} .t-icon img`);
|
||||
if (tileImg) tileImg.src = API.iconUrl(agentId) + ts;
|
||||
} catch (err) {
|
||||
if (hint) {
|
||||
hint.style.color = 'var(--err)';
|
||||
hint.textContent = 'Erreur : ' + (err.message || 'téléversement échoué');
|
||||
setTimeout(() => {
|
||||
hint.style.color = '';
|
||||
hint.textContent = 'Cliquer sur l\'icône pour personnaliser · JPG PNG WEBP · max 128×128 px';
|
||||
}, 4000);
|
||||
}
|
||||
} finally {
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Uptime
|
||||
|
||||
+30
-3
@@ -112,10 +112,33 @@ ok "Répertoire créé"
|
||||
# ── 7. Écrire config.toml ─────────────────────────────────────────────────────
|
||||
echo "[3/5] Écriture de $CONFIG_FILE"
|
||||
|
||||
# Ne pas écraser une config existante (upgrade)
|
||||
OVERWRITE_CONFIG="${OVERWRITE_CONFIG:-}"
|
||||
WRITE_CONFIG=true
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
warn "config.toml déjà présent — conservé tel quel"
|
||||
else
|
||||
if [ "${OVERWRITE_CONFIG}" = "true" ]; then
|
||||
warn "OVERWRITE_CONFIG=true — config.toml sera écrasé"
|
||||
WRITE_CONFIG=true
|
||||
elif [ -t 0 ]; then
|
||||
# Mode interactif (bash local, pas curl | bash)
|
||||
echo ""
|
||||
warn "Un config.toml existe déjà :"
|
||||
echo " $CONFIG_FILE"
|
||||
printf " Écraser la configuration existante ? [o/N] : "
|
||||
read -r _ANS
|
||||
if [[ "$_ANS" =~ ^[Oo]$ ]]; then
|
||||
WRITE_CONFIG=true
|
||||
else
|
||||
ok "config.toml conservé"
|
||||
WRITE_CONFIG=false
|
||||
fi
|
||||
else
|
||||
warn "config.toml déjà présent — conservé (relancez avec OVERWRITE_CONFIG=true pour écraser)"
|
||||
WRITE_CONFIG=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$WRITE_CONFIG" = "true" ]; then
|
||||
cat > "$CONFIG_FILE" << TOML
|
||||
[server]
|
||||
ip = "$SERVER_IP"
|
||||
@@ -165,6 +188,10 @@ TOML
|
||||
ok "config.toml créé"
|
||||
fi
|
||||
|
||||
# S'assurer que le fichier est toujours lisible (cas d'un config existant en 640)
|
||||
chmod 644 "$CONFIG_FILE" 2>/dev/null || true
|
||||
chmod 755 "$CONFIG_DIR"
|
||||
|
||||
# ── 8. Installer le fichier service ──────────────────────────────────────────
|
||||
echo "[4/5] Installation du service systemd"
|
||||
curl -fsSL -o "$SERVICE_FILE" "$SERVICE_URL"
|
||||
|
||||
@@ -11,7 +11,7 @@ RestartSec=5
|
||||
|
||||
DynamicUser=yes
|
||||
ConfigurationDirectory=nanometrics
|
||||
ConfigurationDirectoryMode=0750
|
||||
ConfigurationDirectoryMode=0755
|
||||
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@ DESCRIPTION="${2:-Release $TAG}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CARGO_TOML="$ROOT/agent/Cargo.toml"
|
||||
|
||||
# ── 1. Compiler pour toutes les cibles supportées ──────────────────────────
|
||||
mkdir -p "$ROOT/dist"
|
||||
|
||||
# ── 1. Compiler l'agent pour toutes les cibles supportées ────────────────
|
||||
echo "=== Compilation de l'agent ==="
|
||||
TARGETS=("x86_64-unknown-linux-musl" "aarch64-unknown-linux-musl")
|
||||
LABELS=("linux-amd64" "linux-arm64")
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+4
-5
@@ -5,10 +5,9 @@ RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o nanometrics-server .
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/nanometrics-server .
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/nanometrics-server /nanometrics-server
|
||||
VOLUME /data
|
||||
EXPOSE 8080 9999/udp
|
||||
CMD ["./nanometrics-server"]
|
||||
CMD ["/nanometrics-server"]
|
||||
|
||||
@@ -128,6 +128,72 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
|
||||
return agents, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) {
|
||||
var cpu, temperature *float64
|
||||
var memUsed, memFree, memTotal, hddUsed, hddFree, hddTotal *int64
|
||||
var uptime, netRX, netTX *int64
|
||||
var smartPassed, smartTemp, smartRealloc, smartHours, smartWear *int64
|
||||
|
||||
// Chaque sous-requête prend la dernière valeur NON NULL de sa colonne.
|
||||
// Nécessaire car les paquets rapides (2s) ne contiennent pas les métriques
|
||||
// lentes (disque, smart) qui sont envoyées toutes les 60s.
|
||||
err := d.conn.QueryRow(`
|
||||
SELECT
|
||||
(SELECT cpu_percent FROM metrics WHERE agent_id=? AND cpu_percent IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_used FROM metrics WHERE agent_id=? AND memory_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_free FROM metrics WHERE agent_id=? AND memory_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_total FROM metrics WHERE agent_id=? AND memory_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_used FROM metrics WHERE agent_id=? AND hdd_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_free FROM metrics WHERE agent_id=? AND hdd_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_total FROM metrics WHERE agent_id=? AND hdd_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT uptime FROM metrics WHERE agent_id=? AND uptime IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT network_rx FROM metrics WHERE agent_id=? AND network_rx IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT network_tx FROM metrics WHERE agent_id=? AND network_tx IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT temperature FROM metrics WHERE agent_id=? AND temperature IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_passed FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_temp FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_realloc FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_hours FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_wear FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1)`,
|
||||
agentID, agentID, agentID, agentID,
|
||||
agentID, agentID, agentID,
|
||||
agentID, agentID, agentID, agentID,
|
||||
agentID, agentID, agentID, agentID, agentID).
|
||||
Scan(&cpu, &memUsed, &memFree, &memTotal,
|
||||
&hddUsed, &hddFree, &hddTotal,
|
||||
&uptime, &netRX, &netTX, &temperature,
|
||||
&smartPassed, &smartTemp, &smartRealloc, &smartHours, &smartWear)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := &models.AgentMetrics{
|
||||
CPUPercent: cpu,
|
||||
MemoryUsed: memUsed,
|
||||
MemoryFree: memFree,
|
||||
MemoryTotal: memTotal,
|
||||
HDDUsed: hddUsed,
|
||||
HDDFree: hddFree,
|
||||
HDDTotal: hddTotal,
|
||||
Uptime: uptime,
|
||||
NetworkRX: netRX,
|
||||
NetworkTX: netTX,
|
||||
Temperature: temperature,
|
||||
}
|
||||
if smartPassed != nil {
|
||||
m.Smart = &models.SmartMetrics{
|
||||
Passed: *smartPassed == 1,
|
||||
Temperature: smartTemp,
|
||||
ReallocatedSectors: smartRealloc,
|
||||
PowerOnHours: smartHours,
|
||||
WearLevel: smartWear,
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetMetricsHistory(agentID string, from, to int64) ([]map[string]interface{}, error) {
|
||||
rows, err := d.conn.Query(`
|
||||
SELECT ts, cpu_percent, memory_used, memory_total, hdd_used, hdd_total
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
server:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
pull: false
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
UDP_ADDR: "0.0.0.0:9999"
|
||||
@@ -16,6 +17,7 @@ services:
|
||||
|
||||
dashboard:
|
||||
image: nginx:alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
@@ -15,6 +15,9 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
for i := range agents {
|
||||
agents[i].LastMetrics, _ = database.GetLastMetrics(agents[i].ID)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(agents)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/user/nanometrics/server/db"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
const maxIconSize = 128
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -2,6 +2,7 @@ server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
client_max_body_size 10m;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:8080;
|
||||
|
||||
Reference in New Issue
Block a user