feat(hostinger): VPS resource metrics modal — CPU, RAM, disco, tráfico, uptime

This commit is contained in:
Lizandro Guarnizo
2026-05-14 21:00:57 -05:00
parent 1dd1a67459
commit 277ad6bb63
3 changed files with 144 additions and 3 deletions
+4 -2
View File
@@ -382,8 +382,10 @@ func (c *HostingerClient) SetVPSHostname(vmID int, hostname string) error {
}
// GetVPSMetrics obtiene las métricas de uso de una VM.
func (c *HostingerClient) GetVPSMetrics(vmID int) (json.RawMessage, error) {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/metrics", vmID)
// dateFrom y dateTo deben estar en formato RFC3339, p. ej. "2025-01-01T00:00:00Z".
func (c *HostingerClient) GetVPSMetrics(vmID int, dateFrom, dateTo string) (json.RawMessage, error) {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/metrics?date_from=%s&date_to=%s",
vmID, dateFrom, dateTo)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
+133
View File
@@ -109,6 +109,10 @@
class="flex-1 text-xs py-1.5 rounded border border-gray-300 text-gray-600 hover:bg-gray-50 transition">
✏️ Hostname
</button>
<button @click="openMetrics(v)"
class="flex-1 text-xs py-1.5 rounded border border-blue-200 text-blue-600 hover:bg-blue-50 transition">
📊 Métricas
</button>
</div>
</div>
</template>
@@ -313,6 +317,80 @@
</div>
</div>
<!-- ─── Modal: VPS Métricas ─── -->
<div x-show="metricsModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
@click.self="metricsModal=false">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md">
<div class="flex items-center justify-between px-5 py-4 border-b">
<h3 class="font-semibold text-sm" x-text="'Métricas: ' + (metricsVPS?.hostname || '')"></h3>
<button @click="metricsModal=false" class="text-gray-400 hover:text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-5">
<div x-show="metricsLoading" class="text-center text-sm text-gray-400 py-8">Cargando métricas...</div>
<div x-show="!metricsLoading && metricsData" class="space-y-4">
<!-- CPU -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="font-medium text-gray-700">CPU</span>
<span class="text-gray-500" x-text="avgMetricVal(metricsData?.cpu_usage?.usage).toFixed(1) + '%'"></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="h-2 rounded-full transition-all duration-500"
:class="avgMetricVal(metricsData?.cpu_usage?.usage) > 80 ? 'bg-red-500' : avgMetricVal(metricsData?.cpu_usage?.usage) > 50 ? 'bg-yellow-500' : 'bg-green-500'"
:style="'width:' + Math.min(avgMetricVal(metricsData?.cpu_usage?.usage), 100) + '%'">
</div>
</div>
</div>
<!-- RAM -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="font-medium text-gray-700">RAM</span>
<span class="text-gray-500" x-text="formatBytesRaw(latestMetricVal(metricsData?.ram_usage?.usage)) + ' / ' + formatBytes(metricsVPS?.memory)"></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="h-2 rounded-full bg-blue-500 transition-all duration-500"
:style="'width:' + Math.min((latestMetricVal(metricsData?.ram_usage?.usage) / ((metricsVPS?.memory || 1) * 1024 * 1024)) * 100, 100) + '%'">
</div>
</div>
</div>
<!-- Disco -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="font-medium text-gray-700">Disco</span>
<span class="text-gray-500" x-text="formatBytesRaw(latestMetricVal(metricsData?.disk_space?.usage)) + ' / ' + formatBytes(metricsVPS?.disk)"></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="h-2 rounded-full bg-purple-500 transition-all duration-500"
:style="'width:' + Math.min((latestMetricVal(metricsData?.disk_space?.usage) / ((metricsVPS?.disk || 1) * 1024 * 1024)) * 100, 100) + '%'">
</div>
</div>
</div>
<!-- Red -->
<div class="grid grid-cols-2 gap-3">
<div class="bg-gray-50 rounded-lg p-2.5 text-xs">
<div class="font-medium text-gray-700 mb-0.5">↓ Entrante</div>
<div class="text-gray-500" x-text="formatBytesRaw(sumMetricVal(metricsData?.incoming_traffic?.usage))"></div>
</div>
<div class="bg-gray-50 rounded-lg p-2.5 text-xs">
<div class="font-medium text-gray-700 mb-0.5">↑ Saliente</div>
<div class="text-gray-500" x-text="formatBytesRaw(sumMetricVal(metricsData?.outgoing_traffic?.usage))"></div>
</div>
</div>
<!-- Uptime -->
<div class="flex justify-between text-xs">
<span class="font-medium text-gray-700">Uptime</span>
<span class="text-gray-500" x-text="formatUptime(latestMetricVal(metricsData?.uptime?.usage))"></span>
</div>
<div class="text-xs text-gray-400 text-center pt-1">Promedio últimas 24 horas</div>
</div>
</div>
</div>
</div>
<!-- ─── Modal: Nameservers ─── -->
<div x-show="nsModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
@click.self="nsModal=false">
@@ -505,6 +583,11 @@ document.addEventListener('alpine:init', () => {
// Nameservers
nsModal: false,
nsForm: { domain: '', ns1: '', ns2: '', ns3: '', ns4: '' },
// Métricas VPS
metricsModal: false,
metricsData: null,
metricsVPS: null,
metricsLoading: false,
toast: { show: false, msg: '', type: 'ok' },
async init() {
@@ -629,6 +712,56 @@ document.addEventListener('alpine:init', () => {
this.loading = false;
},
async openMetrics(v) {
this.metricsVPS = v;
this.metricsData = null;
this.metricsModal = true;
this.metricsLoading = true;
try {
const res = await axios.get(`/app/hostinger/vps/${v.id}/metrics`);
this.metricsData = res.data;
} catch (e) {
this.showToast(e.response?.data?.error || 'Error al cargar métricas', 'error');
this.metricsModal = false;
}
this.metricsLoading = false;
},
latestMetricVal(usage) {
if (!usage) return 0;
const keys = Object.keys(usage).sort();
if (!keys.length) return 0;
return usage[keys[keys.length - 1]] || 0;
},
avgMetricVal(usage) {
if (!usage) return 0;
const vals = Object.values(usage);
if (!vals.length) return 0;
return vals.reduce((a, b) => a + b, 0) / vals.length;
},
sumMetricVal(usage) {
if (!usage) return 0;
return Object.values(usage).reduce((a, b) => a + b, 0);
},
formatBytesRaw(bytes) {
if (!bytes || bytes === 0) return '—';
const gb = bytes / (1024 ** 3);
if (gb >= 1) return gb.toFixed(1) + ' GB';
return (bytes / (1024 ** 2)).toFixed(0) + ' MB';
},
formatUptime(ms) {
if (!ms) return '—';
const s = Math.floor(ms / 1000);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
if (h >= 24) return `${Math.floor(h / 24)}d ${h % 24}h`;
return `${h}h ${m}m`;
},
async updateNameservers() {
this.loading = true;
try {
+7 -1
View File
@@ -1,6 +1,8 @@
package controllers
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
@@ -317,6 +319,7 @@ func SetHostingerVPSHostname(c *fiber.Ctx) error {
}
// GetHostingerVPSMetrics retorna las métricas de una VM.
// Acepta query params date_from y date_to (RFC3339). Por defecto: últimas 24 h.
func GetHostingerVPSMetrics(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
@@ -326,7 +329,10 @@ func GetHostingerVPSMetrics(c *fiber.Ctx) error {
if errC != nil {
return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
}
data, err := client.GetVPSMetrics(id)
now := time.Now().UTC()
dateFrom := c.Query("date_from", now.Add(-24*time.Hour).Format(time.RFC3339))
dateTo := c.Query("date_to", now.Format(time.RFC3339))
data, err := client.GetVPSMetrics(id, dateFrom, dateTo)
if err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
}