feat: panel de Actualizaciones en Configuracion (solo informativo)

Compara la version instalada contra el CHANGELOG.md de main en Gitea (lectura anonima por HTTPS,
sin token) y muestra cada version pendiente con su criticidad -- derivada del digito que cambio
(1ro=Alta, 2do=Media, 3ro=Baja, sin etiquetar nada a mano, regla ya documentada en
VERSIONADO.md), una descripcion breve, y si trae migraciones de base de datos nuevas. Banner en
rojo avisando que la actualizacion requiere reiniciar el sistema.

No instala nada automaticamente -- solo informa. Aplicar la actualizacion (git pull, build,
migrar, reiniciar servicios) sigue siendo manual, a criterio del administrador de cada
instalacion, tal cual DEPLOY_WEB.md ya documenta.

Nuevo GET /api/updater/status (solo superadmin). Requiere que el repo en Gitea sea publico con
vista anonima habilitada -- documentado en DEPLOY_WEB.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-16 17:11:58 -05:00
parent 95e3a0199d
commit 59aadc6a35
15 changed files with 273 additions and 4 deletions
@@ -1,5 +1,5 @@
import { Navigate } from 'react-router-dom';
import { Palette, ListChecks, Flag, FolderKanban, Layers, Mail, Building2 } from 'lucide-react';
import { Palette, ListChecks, Flag, FolderKanban, Layers, Mail, Building2, RefreshCw } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAuth } from '../../context/auth-context.jsx';
import { Expander } from '../../components/Expander.jsx';
@@ -8,6 +8,7 @@ import { ComboBox } from '../../components/ComboBox.jsx';
import { CatalogEditor } from './CatalogEditor.jsx';
import SmtpSettingsPanel from './SmtpSettingsPanel.jsx';
import CompanySettingsPanel from './CompanySettingsPanel.jsx';
import UpdaterPanel from './UpdaterPanel.jsx';
export default function ConfiguracionPage() {
const { t, theme, setTheme, lang, setLang, buttonMode, setButtonMode } = useUi();
@@ -101,6 +102,10 @@ export default function ConfiguracionPage() {
<Expander icon={Mail} title={t('config_smtp')}>
<SmtpSettingsPanel />
</Expander>
<Expander icon={RefreshCw} title={t('config_updater')}>
<UpdaterPanel />
</Expander>
</div>
);
}
@@ -0,0 +1,61 @@
import { RefreshCw } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAsync } from '../../hooks/useAsync.js';
import { getUpdateStatus } from '../../services/updater.js';
import { Button } from '../../components/Button.jsx';
import { Badge } from '../../components/Badge.jsx';
import { InfoBar } from '../../components/InfoBar.jsx';
const CRITICALITY_COLOR = { alta: 'danger', media: 'warning', baja: 'success' };
export default function UpdaterPanel() {
const { t } = useUi();
const { data: status, loading, error, reload } = useAsync(getUpdateStatus, []);
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
<Button variant="standard" icon={RefreshCw} onClick={reload} disabled={loading}>
{t('updater_check')}
</Button>
{status && <span className="text-muted" style={{ fontSize: '.78rem' }}>{t('updater_current_version')} v{status.currentVersion}</span>}
</div>
{loading && <div className="text-muted">{t('common_loading')}</div>}
{error && <InfoBar tone="error" dismissible={false}>{error}</InfoBar>}
{status && status.upToDate && (
<InfoBar tone="success" dismissible={false}>{t('updater_up_to_date')}</InfoBar>
)}
{status && !status.upToDate && (
<div>
<InfoBar tone="error" dismissible={false}>{t('updater_restart_required')}</InfoBar>
{status.hasMigrations && (
<InfoBar tone="warning" dismissible={false}>{t('updater_has_migrations')}</InfoBar>
)}
<div className="panel-card">
<h4 style={{ marginTop: 0 }}>
{t('updater_available')} — v{status.currentVersion} → v{status.latestVersion}
{' '}
<Badge color={CRITICALITY_COLOR[status.overallCriticality]}>{t(`updater_criticality_${status.overallCriticality}`)}</Badge>
</h4>
{status.pending.map((entry) => (
<div key={entry.version} style={{ padding: '10px 0', borderTop: '1px solid rgb(var(--line))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<strong>v{entry.version}</strong>
<span className="text-muted" style={{ fontSize: '.72rem' }}>{entry.date}</span>
<Badge color={CRITICALITY_COLOR[entry.criticality]}>{t(`updater_criticality_${entry.criticality}`)}</Badge>
</div>
<div style={{ fontSize: '.8rem', color: 'rgb(var(--muted))' }}>{entry.summary}</div>
</div>
))}
</div>
<div className="text-muted" style={{ fontSize: '.76rem' }}>{t('updater_rollback_hint')}</div>
</div>
)}
</div>
);
}