feat(hostinger): VPS resource metrics modal — CPU, RAM, disco, tráfico, uptime
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user