feat(settings): add about system information
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { settingsApi, type StoreSettings } from '@/lib/api-client';
|
||||
import { settingsApi, type AboutInfo, type StoreSettings } from '@/lib/api-client';
|
||||
|
||||
type FormData = StoreSettings;
|
||||
|
||||
@@ -12,6 +12,7 @@ const TABS = [
|
||||
{ id: 'ai', label: 'IA para SEO', icon: '✨' },
|
||||
{ id: 'smtp', label: 'SMTP / Email', icon: '✉️' },
|
||||
{ id: 'couriers', label: 'Transportistas', icon: '🚚' },
|
||||
{ id: 'about', label: 'About', icon: 'ℹ️' },
|
||||
] as const;
|
||||
type TabId = (typeof TABS)[number]['id'];
|
||||
|
||||
@@ -24,12 +25,28 @@ export default function SettingsPage() {
|
||||
const [err, setErr] = useState('');
|
||||
const [tab, setTab] = useState<TabId>('general');
|
||||
const [couriersText, setCouriersText] = useState('');
|
||||
const [about, setAbout] = useState<AboutInfo | null>(null);
|
||||
const [aboutLoading, setAboutLoading] = useState(false);
|
||||
|
||||
const loadAbout = async () => {
|
||||
setAboutLoading(true);
|
||||
try {
|
||||
setAbout(await settingsApi.about());
|
||||
} catch {
|
||||
setErr('Error al cargar información del sistema');
|
||||
} finally {
|
||||
setAboutLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
settingsApi.get().then(d => {
|
||||
setData(d); setForm(d);
|
||||
setCouriersText((d.couriers ?? []).join('\n'));
|
||||
}).catch(() => setErr('Error al cargar ajustes')).finally(() => setLoading(false));
|
||||
Promise.all([
|
||||
settingsApi.get().then(d => {
|
||||
setData(d); setForm(d);
|
||||
setCouriersText((d.couriers ?? []).join('\n'));
|
||||
}),
|
||||
loadAbout(),
|
||||
]).catch(() => setErr('Error al cargar ajustes')).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleCouriersChange = (text: string) => {
|
||||
@@ -38,6 +55,22 @@ export default function SettingsPage() {
|
||||
setForm(f => f ? { ...f, couriers: list } : f);
|
||||
};
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const infoRow = (label: string, value: string | number) => (
|
||||
<div className="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-gray-400">{label}</p>
|
||||
<p className="mt-1 break-words text-sm font-semibold text-gray-800">{value}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form) return;
|
||||
@@ -228,6 +261,81 @@ export default function SettingsPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'about' && (
|
||||
<>
|
||||
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-gray-800">About / Sistema</h2>
|
||||
<p className="text-xs text-gray-400 mt-0.5">Versiones de servicios e información segura del servidor.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadAbout()}
|
||||
disabled={aboutLoading}
|
||||
className="rounded-xl border border-gray-300 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-white disabled:opacity-50"
|
||||
>
|
||||
{aboutLoading ? 'Actualizando…' : 'Actualizar'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{about ? (
|
||||
<>
|
||||
<div className="rounded-2xl border border-[#2D6A4F]/20 bg-[#2D6A4F]/5 p-5">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-[#2D6A4F]">Versión producto</p>
|
||||
<p className="mt-1 text-3xl font-bold text-[#1B4332]">v{about.productVersion}</p>
|
||||
<p className="mt-2 text-xs text-gray-500">Generado: {new Date(about.generatedAt).toLocaleString('es-ES')}</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-800">Servicios</h3>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{about.services.map(service => (
|
||||
<div key={service.id} className="rounded-xl border border-gray-200 p-4">
|
||||
<p className="text-sm font-semibold text-gray-900">{service.name}</p>
|
||||
<p className="mt-1 font-mono text-xs text-gray-500">{service.id}</p>
|
||||
<p className="mt-3 inline-flex rounded-full bg-gray-100 px-2.5 py-1 font-mono text-xs font-semibold text-gray-700">v{service.version}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-800">Servidor</h3>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{infoRow('Host', about.server.hostname)}
|
||||
{infoRow('Sistema', `${about.server.platform} ${about.server.arch}`)}
|
||||
{infoRow('Kernel / Release', about.server.release)}
|
||||
{infoRow('Node.js', about.server.nodeVersion)}
|
||||
{infoRow('PID backend', about.server.pid)}
|
||||
{infoRow('Zona horaria', about.server.timezone)}
|
||||
{infoRow('Uptime host', formatDuration(about.server.uptimeSeconds))}
|
||||
{infoRow('Uptime proceso', formatDuration(about.server.processUptimeSeconds))}
|
||||
{infoRow('Arranque proceso', new Date(about.server.processStartedAt).toLocaleString('es-ES'))}
|
||||
{infoRow('CPU cores', about.server.cpuCount)}
|
||||
{infoRow('RAM total', `${about.server.totalMemoryMb} MB`)}
|
||||
{infoRow('RAM libre', `${about.server.freeMemoryMb} MB`)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-800">Proceso y base de datos</h3>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{infoRow('RSS proceso', `${about.server.processMemoryMb.rss} MB`)}
|
||||
{infoRow('Heap usado', `${about.server.processMemoryMb.heapUsed} MB`)}
|
||||
{infoRow('Heap total', `${about.server.processMemoryMb.heapTotal} MB`)}
|
||||
{infoRow('PostgreSQL', about.database.postgresVersion)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-xl border border-gray-200 p-8 text-center text-sm text-gray-500">
|
||||
{aboutLoading ? 'Cargando información del sistema…' : 'No se pudo cargar la información del sistema.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'footer' && (
|
||||
<>
|
||||
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||
@@ -244,12 +352,14 @@ export default function SettingsPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="px-6 py-5 bg-gray-50 border-t border-gray-200 flex justify-end">
|
||||
<button type="submit" disabled={saving || !form}
|
||||
className="px-6 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors">
|
||||
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||
</button>
|
||||
</div>
|
||||
{tab !== 'about' && (
|
||||
<div className="px-6 py-5 bg-gray-50 border-t border-gray-200 flex justify-end">
|
||||
<button type="submit" disabled={saving || !form}
|
||||
className="px-6 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors">
|
||||
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user