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"`
|
||||
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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user