This commit is contained in:
Lizandro Guarnizo
2026-05-21 22:52:24 -05:00
parent 0cf7c6d279
commit d121f399ed
5 changed files with 77 additions and 26 deletions
+3 -2
View File
@@ -27,8 +27,9 @@ type Servidor struct {
AgentLastSeen *time.Time `json:"agent_last_seen" gorm:"column:agent_last_seen"`
MetricasJson string `json:"metricas_json" gorm:"column:metricas_json;type:text"`
// Integración Hostinger
HostingerVpsID *int `json:"hostinger_vps_id" gorm:"column:hostinger_vps_id"`
HostingerState string `json:"hostinger_state" gorm:"column:hostinger_state"`
HostingerVpsID *int `json:"hostinger_vps_id" gorm:"column:hostinger_vps_id"`
HostingerState string `json:"hostinger_state" gorm:"column:hostinger_state"`
HostingerSubscriptionID string `json:"hostinger_subscription_id" gorm:"column:hostinger_subscription_id"`
}
func (Servidor) TableName() string {
+25 -10
View File
@@ -111,16 +111,17 @@ type HostingerIPAddress struct {
// HostingerVPS representa una máquina virtual VPS.
type HostingerVPS struct {
ID int `json:"id"`
Hostname string `json:"hostname"`
State string `json:"state"`
Plan string `json:"plan"`
DataCenter string `json:"data_center"`
CPU int `json:"cpus"`
RAMBytes int64 `json:"memory"`
DiskBytes int64 `json:"disk"`
IPV4 []HostingerIPAddress `json:"ipv4"`
IPV6 []HostingerIPAddress `json:"ipv6"`
ID int `json:"id"`
Hostname string `json:"hostname"`
State string `json:"state"`
Plan string `json:"plan"`
DataCenter string `json:"data_center"`
CPU int `json:"cpus"`
RAMBytes int64 `json:"memory"`
DiskBytes int64 `json:"disk"`
IPV4 []HostingerIPAddress `json:"ipv4"`
IPV6 []HostingerIPAddress `json:"ipv6"`
SubscriptionID string `json:"subscription_id"`
}
// HostingerDomain representa un dominio en el portafolio.
@@ -303,6 +304,20 @@ func (c *HostingerClient) GetDomainNameServers(domain string) (*HostingerNameSer
return &resp.NameServers, nil
}
// GetSubscriptionByID obtiene una suscripción de billing por su ID.
func (c *HostingerClient) GetSubscriptionByID(subID string) (*HostingerSubscription, error) {
path := fmt.Sprintf("/billing/v1/subscriptions/%s", subID)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
}
var sub HostingerSubscription
if err := json.Unmarshal(body, &sub); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar suscripción: %w", err)
}
return &sub, nil
}
// GetOrders obtiene las suscripciones de facturación.
func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
body, err := c.getRaw(context.Background(), "/billing/v1/subscriptions")
+14 -6
View File
@@ -275,15 +275,21 @@
<input type="number" x-model="selectedItem.hostinger_vps_id"
class="border border-gray-300 rounded p-2 w-full" placeholder="ID del VPS en Hostinger" />
</div>
<div class="w-full flex items-end" x-show="selectedItem.id && selectedItem.hostinger_vps_id">
<button @click="syncHostinger(selectedItem.id)" :disabled="syncCargando[selectedItem.id]"
class="w-full px-4 py-2 bg-orange-500 text-white rounded text-sm flex items-center justify-center gap-2 hover:bg-orange-600 disabled:opacity-50">
<svg :class="syncCargando[selectedItem.id] ? 'animate-spin' : ''" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<span x-text="syncCargando[selectedItem.id] ? 'Sincronizando...' : 'Sync Hostinger'"></span>
</button>
<div class="w-full">
<label class="block mb-2">Hostinger Subscription ID <span class="text-xs text-gray-400">(billing)</span>:</label>
<input type="text" x-model="selectedItem.hostinger_subscription_id"
class="border border-gray-300 rounded p-2 w-full font-mono text-xs" placeholder="Auto-detectado al sincronizar" />
</div>
</div>
<div class="w-full mb-4" x-show="selectedItem.id && selectedItem.hostinger_vps_id">
<button @click="syncHostinger(selectedItem.id)" :disabled="syncCargando[selectedItem.id]"
class="w-full px-4 py-2 bg-orange-500 text-white rounded text-sm flex items-center justify-center gap-2 hover:bg-orange-600 disabled:opacity-50">
<svg :class="syncCargando[selectedItem.id] ? 'animate-spin' : ''" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<span x-text="syncCargando[selectedItem.id] ? 'Sincronizando...' : 'Sync desde Hostinger (IP, RAM, CPU, Disco, Vencimiento)'"></span>
</button>
</div>
<div class="flex justify-between gap-2 text-sm">
<button @click="updateItem()" class="px-4 py-2 bg-[#8eb02f] text-white rounded">Actualizar</button>
<button @click="closeModals()" class="px-4 py-2 bg-gray-600 text-white rounded">Cerrar</button>
@@ -454,6 +460,7 @@
tipo_servidor_id: data.tipo_servidor_id,
hostinger_vps_id: data.hostinger_vps_id,
hostinger_state: data.hostinger_state,
hostinger_subscription_id: data.hostinger_subscription_id,
created_at: data.CreatedAt,
}));
console.log('📊 Servidores procesados:', this.datos.length);
@@ -625,6 +632,7 @@
prov_servidor_id: this.selectedItem.prov_servidor_id,
tipo_servidor_id: this.selectedItem.tipo_servidor_id,
hostinger_vps_id: this.selectedItem.hostinger_vps_id ? parseInt(this.selectedItem.hostinger_vps_id) : null,
hostinger_subscription_id: this.selectedItem.hostinger_subscription_id || '',
};
this.setLoading(true);
+10
View File
@@ -244,6 +244,16 @@
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.vencimiento || '—'"></p>
</div>
<div x-show="servidorSeleccionado?.hostinger_vps_id" class="bg-orange-50 border border-orange-200 rounded-xl p-4">
<p class="text-xs text-orange-500 font-medium mb-1">🔗 Hostinger VPS</p>
<p class="font-mono font-bold text-slate-900 text-sm">
ID: <span x-text="servidorSeleccionado?.hostinger_vps_id"></span>
<span x-show="servidorSeleccionado?.hostinger_subscription_id" class="block text-xs text-slate-500 font-normal mt-0.5">
Sub: <span x-text="servidorSeleccionado?.hostinger_subscription_id" class="font-mono"></span>
</span>
</p>
</div>
<div x-show="servidorSeleccionado?.ultimo_ping" class="bg-slate-50 border border-slate-200 rounded-xl p-4 md:col-span-2">
<p class="text-xs text-slate-400 font-medium mb-1">📡 Último Ping</p>
<p class="font-bold text-slate-900 text-sm font-mono" x-text="servidorSeleccionado?.ultimo_ping"></p>
+25 -8
View File
@@ -232,14 +232,31 @@ func SyncServidorFromHostinger(c *fiber.Ctx) error {
updates["disco"] = fmt.Sprintf("%d GB", vps.DiskBytes)
}
// Vencimiento: buscar suscripción cuyo nombre contenga el hostname del VPS
if subs, sErr := client.GetOrders(); sErr == nil {
for _, sub := range subs {
if sub.ExpiresAt != "" && len(sub.ExpiresAt) >= 10 &&
(strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) ||
strings.Contains(strings.ToLower(sub.Name), "vps")) {
updates["vencimiento"] = sub.ExpiresAt[:10]
break
// Vencimiento: resolución en cascada
// 1) subscription_id que viene directo del VPS (el más confiable)
// 2) subscription_id ya guardado en el servidor local
// 3) fallback fuzzy por nombre
subID := vps.SubscriptionID
if subID == "" && servidor.HostingerSubscriptionID != "" {
subID = servidor.HostingerSubscriptionID
}
if subID != "" {
// Lookup directo — sin ambigüedad
if sub, sErr := client.GetSubscriptionByID(subID); sErr == nil && sub.ExpiresAt != "" && len(sub.ExpiresAt) >= 10 {
updates["vencimiento"] = sub.ExpiresAt[:10]
updates["hostinger_subscription_id"] = subID
}
} else {
// Fallback: buscar por nombre si aún no tenemos subscription_id
if subs, sErr := client.GetOrders(); sErr == nil {
for _, sub := range subs {
if sub.ExpiresAt != "" && len(sub.ExpiresAt) >= 10 &&
strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) {
updates["vencimiento"] = sub.ExpiresAt[:10]
updates["hostinger_subscription_id"] = sub.ID
break
}
}
}
}