From ed031f8a941de7783506ab9506d9732fbda03d95 Mon Sep 17 00:00:00 2001 From: Deploy Date: Tue, 25 Aug 2026 22:08:20 +0200 Subject: [PATCH] feat(settings): add about system information --- project/VERSION | 2 +- project/apps/admin/package-lock.json | 4 +- project/apps/admin/package.json | 2 +- .../src/app/(dashboard)/settings/page.tsx | 132 ++++++++++++++++-- project/apps/admin/src/lib/api-client.ts | 24 ++++ project/apps/pos/package-lock.json | 4 +- project/apps/pos/package.json | 2 +- project/frontend/package-lock.json | 4 +- project/frontend/package.json | 2 +- project/package-lock.json | 4 +- project/package.json | 2 +- .../store-settings/api/settings.routes.ts | 94 +++++++++++++ project/storefront/package-lock.json | 4 +- project/storefront/package.json | 2 +- 14 files changed, 255 insertions(+), 27 deletions(-) diff --git a/project/VERSION b/project/VERSION index 6e8bf73..0ea3a94 100644 --- a/project/VERSION +++ b/project/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/project/apps/admin/package-lock.json b/project/apps/admin/package-lock.json index be67c14..a415e4e 100644 --- a/project/apps/admin/package-lock.json +++ b/project/apps/admin/package-lock.json @@ -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", diff --git a/project/apps/admin/package.json b/project/apps/admin/package.json index 8562ac6..87031b7 100644 --- a/project/apps/admin/package.json +++ b/project/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "@mercadodevida/admin", - "version": "0.1.0", + "version": "0.2.0", "private": true, "scripts": { "dev": "next dev --port 3001", diff --git a/project/apps/admin/src/app/(dashboard)/settings/page.tsx b/project/apps/admin/src/app/(dashboard)/settings/page.tsx index 56e8127..803015a 100644 --- a/project/apps/admin/src/app/(dashboard)/settings/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/settings/page.tsx @@ -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('general'); const [couriersText, setCouriersText] = useState(''); + const [about, setAbout] = useState(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) => ( +
+

{label}

+

{value}

+
+ ); + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); if (!form) return; @@ -228,6 +261,81 @@ export default function SettingsPage() { )} + {tab === 'about' && ( + <> +
+
+

About / Sistema

+

Versiones de servicios e información segura del servidor.

+
+ +
+
+ {about ? ( + <> +
+

Versión producto

+

v{about.productVersion}

+

Generado: {new Date(about.generatedAt).toLocaleString('es-ES')}

+
+ +
+

Servicios

+
+ {about.services.map(service => ( +
+

{service.name}

+

{service.id}

+

v{service.version}

+
+ ))} +
+
+ +
+

Servidor

+
+ {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`)} +
+
+ +
+

Proceso y base de datos

+
+ {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)} +
+
+ + ) : ( +
+ {aboutLoading ? 'Cargando información del sistema…' : 'No se pudo cargar la información del sistema.'} +
+ )} +
+ + )} + {tab === 'footer' && ( <>
@@ -244,12 +352,14 @@ export default function SettingsPage() { )} -
- -
+ {tab !== 'about' && ( +
+ +
+ )}
)} diff --git a/project/apps/admin/src/lib/api-client.ts b/project/apps/admin/src/lib/api-client.ts index ac9b1b7..5cdcfe5 100644 --- a/project/apps/admin/src/lib/api-client.ts +++ b/project/apps/admin/src/lib/api-client.ts @@ -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('/api/admin/settings'), update: (data: Partial) => api.patch('/api/admin/settings', data), + about: () => api.get('/api/admin/about'), }; diff --git a/project/apps/pos/package-lock.json b/project/apps/pos/package-lock.json index 90d5ee2..bda53d0 100644 --- a/project/apps/pos/package-lock.json +++ b/project/apps/pos/package-lock.json @@ -1,12 +1,12 @@ { "name": "mercadodevida-pos", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mercadodevida-pos", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "next": "^16.3.1", "react": "^19.2.8", diff --git a/project/apps/pos/package.json b/project/apps/pos/package.json index 2b06c5a..9136a7f 100644 --- a/project/apps/pos/package.json +++ b/project/apps/pos/package.json @@ -1,6 +1,6 @@ { "name": "mercadodevida-pos", - "version": "0.1.0", + "version": "0.2.0", "private": true, "scripts": { "dev": "next dev --port 3002", diff --git a/project/frontend/package-lock.json b/project/frontend/package-lock.json index 4ced0d5..a1f31a0 100644 --- a/project/frontend/package-lock.json +++ b/project/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "next": "16.3.1", "react": "19.2.8", diff --git a/project/frontend/package.json b/project/frontend/package.json index 95852f7..99f4924 100644 --- a/project/frontend/package.json +++ b/project/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.1.0", + "version": "0.2.0", "private": true, "scripts": { "dev": "next dev", diff --git a/project/package-lock.json b/project/package-lock.json index 32c904c..c332194 100644 --- a/project/package-lock.json +++ b/project/package-lock.json @@ -1,12 +1,12 @@ { "name": "mercadodevida-backend", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mercadodevida-backend", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/cors": "^11.3.0", diff --git a/project/package.json b/project/package.json index dc6c024..598163e 100644 --- a/project/package.json +++ b/project/package.json @@ -1,6 +1,6 @@ { "name": "mercadodevida-backend", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "description": "mercadodevida vNext backend - modular monolith skeleton", diff --git a/project/src/modules/store-settings/api/settings.routes.ts b/project/src/modules/store-settings/api/settings.routes.ts index 9e39abe..1578705 100644 --- a/project/src/modules/store-settings/api/settings.routes.ts +++ b/project/src/modules/store-settings/api/settings.routes.ts @@ -1,3 +1,6 @@ +import { readFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import type { FastifyInstance } from 'fastify'; import type { FastifySchema } from 'fastify'; import type pg from 'pg'; @@ -62,6 +65,50 @@ export function parseCouriers(raw: string | undefined | null): string[] { } } +type ServiceVersion = { + id: string; + name: string; + version: string; +}; + +function readTextIfExists(filePath: string): string | null { + try { + return readFileSync(filePath, 'utf8').trim(); + } catch { + return null; + } +} + +function readPackageVersion(id: string, label: string, packagePath: string): ServiceVersion { + try { + const raw = readFileSync(packagePath, 'utf8'); + const pkg = JSON.parse(raw) as { name?: string; version?: string }; + return { + id, + name: pkg.name ?? label, + version: pkg.version ?? 'unknown', + }; + } catch { + return { id, name: label, version: 'unknown' }; + } +} + +function collectServiceVersions(): { productVersion: string; services: ServiceVersion[] } { + const root = process.cwd(); + return { + productVersion: readTextIfExists(path.join(root, 'VERSION')) ?? 'unknown', + services: [ + readPackageVersion('backend', 'Backend API', path.join(root, 'package.json')), + readPackageVersion('admin', 'Admin', path.join(root, 'apps/admin/package.json')), + readPackageVersion('tpv', 'TPV', path.join(root, 'apps/pos/package.json')), + readPackageVersion('frontend', 'Frontend', path.join(root, 'frontend/package.json')), + readPackageVersion('storefront', 'Storefront SEO', path.join(root, 'storefront/package.json')), + ], + }; +} + +const PROCESS_STARTED_AT = new Date(); + const SETTING_KEYS: Record = { storeName: 'store_name', storeTagline: 'store_tagline', @@ -96,6 +143,53 @@ export async function registerStoreSettingsRoutes( app: FastifyInstance, deps: StoreSettingsRoutesDeps, ): Promise { + // GET /admin/about — versions and safe server diagnostics for admin UI + const getAboutSchema: FastifySchema = { + tags: ['Admin'], + summary: 'Get service versions and server info (admin)', + response: { 401: errorSchema, 403: errorSchema }, + }; + app.get('/admin/about', { schema: getAboutSchema }, async (request, reply) => { + const user = await deps.authenticate(request); + requireRole(user, 'admin'); + + const { productVersion, services } = collectServiceVersions(); + const dbVersion = await deps.pool + .query<{ server_version: string }>('SHOW server_version') + .then((result) => result.rows[0]?.server_version ?? 'unknown') + .catch(() => 'unavailable'); + const memory = process.memoryUsage(); + + return reply.send({ + productVersion, + generatedAt: new Date().toISOString(), + services, + server: { + hostname: os.hostname(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + uptimeSeconds: Math.floor(os.uptime()), + processUptimeSeconds: Math.floor(process.uptime()), + processStartedAt: PROCESS_STARTED_AT.toISOString(), + nodeVersion: process.version, + pid: process.pid, + cpuCount: os.cpus().length, + totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024), + freeMemoryMb: Math.round(os.freemem() / 1024 / 1024), + processMemoryMb: { + rss: Math.round(memory.rss / 1024 / 1024), + heapUsed: Math.round(memory.heapUsed / 1024 / 1024), + heapTotal: Math.round(memory.heapTotal / 1024 / 1024), + }, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + database: { + postgresVersion: dbVersion, + }, + }); + }); + // GET /admin/settings — fetch all settings const getSettingsSchema: FastifySchema = { tags: ['Admin'], diff --git a/project/storefront/package-lock.json b/project/storefront/package-lock.json index e6e7387..75bec62 100644 --- a/project/storefront/package-lock.json +++ b/project/storefront/package-lock.json @@ -1,12 +1,12 @@ { "name": "mercadodevida-storefront", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mercadodevida-storefront", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@tailwindcss/postcss": "^4.1.17", "next": "^16.0.5", diff --git a/project/storefront/package.json b/project/storefront/package.json index 86c57a2..aac2d5b 100644 --- a/project/storefront/package.json +++ b/project/storefront/package.json @@ -1,6 +1,6 @@ { "name": "mercadodevida-storefront", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "description": "mercadodevida customer storefront shell",