feat(settings): add about system information
This commit is contained in:
4
project/apps/admin/package-lock.json
generated
4
project/apps/admin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@mercadodevida/admin",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@mercadodevida/admin",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@lexical/history": "^0.49.0",
|
||||
"@lexical/html": "^0.49.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mercadodevida/admin",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3001",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -395,7 +395,31 @@ export interface StoreSettings {
|
||||
couriers?: string[];
|
||||
}
|
||||
|
||||
export interface AboutInfo {
|
||||
productVersion: string;
|
||||
generatedAt: string;
|
||||
services: Array<{ id: string; name: string; version: string }>;
|
||||
server: {
|
||||
hostname: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
release: string;
|
||||
uptimeSeconds: number;
|
||||
processUptimeSeconds: number;
|
||||
processStartedAt: string;
|
||||
nodeVersion: string;
|
||||
pid: number;
|
||||
cpuCount: number;
|
||||
totalMemoryMb: number;
|
||||
freeMemoryMb: number;
|
||||
processMemoryMb: { rss: number; heapUsed: number; heapTotal: number };
|
||||
timezone: string;
|
||||
};
|
||||
database: { postgresVersion: string };
|
||||
}
|
||||
|
||||
export const settingsApi = {
|
||||
get: () => api.get<StoreSettings>('/api/admin/settings'),
|
||||
update: (data: Partial<StoreSettings>) => api.patch<StoreSettings>('/api/admin/settings', data),
|
||||
about: () => api.get<AboutInfo>('/api/admin/about'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user