up
This commit is contained in:
@@ -27,8 +27,9 @@ type Servidor struct {
|
|||||||
AgentLastSeen *time.Time `json:"agent_last_seen" gorm:"column:agent_last_seen"`
|
AgentLastSeen *time.Time `json:"agent_last_seen" gorm:"column:agent_last_seen"`
|
||||||
MetricasJson string `json:"metricas_json" gorm:"column:metricas_json;type:text"`
|
MetricasJson string `json:"metricas_json" gorm:"column:metricas_json;type:text"`
|
||||||
// Integración Hostinger
|
// Integración Hostinger
|
||||||
HostingerVpsID *int `json:"hostinger_vps_id" gorm:"column:hostinger_vps_id"`
|
HostingerVpsID *int `json:"hostinger_vps_id" gorm:"column:hostinger_vps_id"`
|
||||||
HostingerState string `json:"hostinger_state" gorm:"column:hostinger_state"`
|
HostingerState string `json:"hostinger_state" gorm:"column:hostinger_state"`
|
||||||
|
HostingerSubscriptionID string `json:"hostinger_subscription_id" gorm:"column:hostinger_subscription_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Servidor) TableName() string {
|
func (Servidor) TableName() string {
|
||||||
|
|||||||
@@ -111,16 +111,17 @@ type HostingerIPAddress struct {
|
|||||||
|
|
||||||
// HostingerVPS representa una máquina virtual VPS.
|
// HostingerVPS representa una máquina virtual VPS.
|
||||||
type HostingerVPS struct {
|
type HostingerVPS struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Plan string `json:"plan"`
|
Plan string `json:"plan"`
|
||||||
DataCenter string `json:"data_center"`
|
DataCenter string `json:"data_center"`
|
||||||
CPU int `json:"cpus"`
|
CPU int `json:"cpus"`
|
||||||
RAMBytes int64 `json:"memory"`
|
RAMBytes int64 `json:"memory"`
|
||||||
DiskBytes int64 `json:"disk"`
|
DiskBytes int64 `json:"disk"`
|
||||||
IPV4 []HostingerIPAddress `json:"ipv4"`
|
IPV4 []HostingerIPAddress `json:"ipv4"`
|
||||||
IPV6 []HostingerIPAddress `json:"ipv6"`
|
IPV6 []HostingerIPAddress `json:"ipv6"`
|
||||||
|
SubscriptionID string `json:"subscription_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostingerDomain representa un dominio en el portafolio.
|
// HostingerDomain representa un dominio en el portafolio.
|
||||||
@@ -303,6 +304,20 @@ func (c *HostingerClient) GetDomainNameServers(domain string) (*HostingerNameSer
|
|||||||
return &resp.NameServers, nil
|
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.
|
// GetOrders obtiene las suscripciones de facturación.
|
||||||
func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
|
func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
|
||||||
body, err := c.getRaw(context.Background(), "/billing/v1/subscriptions")
|
body, err := c.getRaw(context.Background(), "/billing/v1/subscriptions")
|
||||||
|
|||||||
@@ -275,15 +275,21 @@
|
|||||||
<input type="number" x-model="selectedItem.hostinger_vps_id"
|
<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" />
|
class="border border-gray-300 rounded p-2 w-full" placeholder="ID del VPS en Hostinger" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex items-end" x-show="selectedItem.id && selectedItem.hostinger_vps_id">
|
<div class="w-full">
|
||||||
<button @click="syncHostinger(selectedItem.id)" :disabled="syncCargando[selectedItem.id]"
|
<label class="block mb-2">Hostinger Subscription ID <span class="text-xs text-gray-400">(billing)</span>:</label>
|
||||||
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">
|
<input type="text" x-model="selectedItem.hostinger_subscription_id"
|
||||||
<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>
|
class="border border-gray-300 rounded p-2 w-full font-mono text-xs" placeholder="Auto-detectado al sincronizar" />
|
||||||
<span x-text="syncCargando[selectedItem.id] ? 'Sincronizando...' : 'Sync Hostinger'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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="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>
|
<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,
|
tipo_servidor_id: data.tipo_servidor_id,
|
||||||
hostinger_vps_id: data.hostinger_vps_id,
|
hostinger_vps_id: data.hostinger_vps_id,
|
||||||
hostinger_state: data.hostinger_state,
|
hostinger_state: data.hostinger_state,
|
||||||
|
hostinger_subscription_id: data.hostinger_subscription_id,
|
||||||
created_at: data.CreatedAt,
|
created_at: data.CreatedAt,
|
||||||
}));
|
}));
|
||||||
console.log('📊 Servidores procesados:', this.datos.length);
|
console.log('📊 Servidores procesados:', this.datos.length);
|
||||||
@@ -625,6 +632,7 @@
|
|||||||
prov_servidor_id: this.selectedItem.prov_servidor_id,
|
prov_servidor_id: this.selectedItem.prov_servidor_id,
|
||||||
tipo_servidor_id: this.selectedItem.tipo_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_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);
|
this.setLoading(true);
|
||||||
|
|||||||
@@ -244,6 +244,16 @@
|
|||||||
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.vencimiento || '—'"></p>
|
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.vencimiento || '—'"></p>
|
||||||
</div>
|
</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">
|
<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="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>
|
<p class="font-bold text-slate-900 text-sm font-mono" x-text="servidorSeleccionado?.ultimo_ping"></p>
|
||||||
|
|||||||
@@ -232,14 +232,31 @@ func SyncServidorFromHostinger(c *fiber.Ctx) error {
|
|||||||
updates["disco"] = fmt.Sprintf("%d GB", vps.DiskBytes)
|
updates["disco"] = fmt.Sprintf("%d GB", vps.DiskBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vencimiento: buscar suscripción cuyo nombre contenga el hostname del VPS
|
// Vencimiento: resolución en cascada
|
||||||
if subs, sErr := client.GetOrders(); sErr == nil {
|
// 1) subscription_id que viene directo del VPS (el más confiable)
|
||||||
for _, sub := range subs {
|
// 2) subscription_id ya guardado en el servidor local
|
||||||
if sub.ExpiresAt != "" && len(sub.ExpiresAt) >= 10 &&
|
// 3) fallback fuzzy por nombre
|
||||||
(strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) ||
|
subID := vps.SubscriptionID
|
||||||
strings.Contains(strings.ToLower(sub.Name), "vps")) {
|
if subID == "" && servidor.HostingerSubscriptionID != "" {
|
||||||
updates["vencimiento"] = sub.ExpiresAt[:10]
|
subID = servidor.HostingerSubscriptionID
|
||||||
break
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user