feat: version 2.2.0 -- boton real de actualizacion (auto-update)

POST /api/updater/apply dispara scripts/apply-update.sh (backup +
git pull + migraciones + build + restart) en un proceso separado.
Apagado por defecto: solo se activa si el servidor define
UPDATE_APP_DIR/UPDATE_BACKEND_SERVICE/UPDATE_FRONTEND_SERVICE en su
backend/.env. Sin esa config, el panel se comporta igual que antes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-22 12:18:12 -05:00
parent 43fef6048e
commit fa4ddbe164
15 changed files with 249 additions and 12 deletions
+16
View File
@@ -82,6 +82,14 @@ export const messages = {
updater_criticality_media: 'Media',
updater_criticality_baja: 'Baja',
updater_rollback_hint: 'Antes de actualizar, respalda la base de datos. Si algo sale mal, restaura ese respaldo y volvé al commit anterior a esta actualización en el historial de Gitea.',
updater_apply: 'Actualizar',
updater_applying: 'Actualizando…',
updater_apply_confirm_title: 'Aplicar actualización',
updater_apply_confirm_msg: 'Se va a respaldar la base de datos, descargar el código nuevo, aplicar migraciones si las hay, y reiniciar el sistema. Puede tardar unos minutos y la app quedará momentáneamente fuera de servicio. Vas de',
updater_apply_confirm_ok: 'Sí, actualizar',
updater_apply_started: 'Actualización iniciada. El sistema se reiniciará en unos minutos — refrescá la página luego.',
updater_applying_hint: 'Actualización en curso — no cierres esta pestaña. El sistema se reiniciará solo al terminar.',
updater_apply_not_configured: 'La auto-actualización no está habilitada en este servidor. Pedile a tu administrador que aplique esta actualización por SSH.',
config_error_logs: 'Errores del sistema',
error_logs_clear: 'Limpiar registro',
@@ -364,6 +372,14 @@ export const messages = {
updater_criticality_media: 'Medium',
updater_criticality_baja: 'Low',
updater_rollback_hint: 'Back up the database before updating. If something goes wrong, restore that backup and roll back to the commit before this update in the Gitea history.',
updater_apply: 'Update',
updater_applying: 'Updating…',
updater_apply_confirm_title: 'Apply update',
updater_apply_confirm_msg: 'This will back up the database, pull the new code, apply migrations if any, and restart the system. It may take a few minutes and the app will be briefly unavailable. Going from',
updater_apply_confirm_ok: 'Yes, update',
updater_apply_started: 'Update started. The system will restart in a few minutes — refresh the page afterward.',
updater_applying_hint: 'Update in progress — do not close this tab. The system will restart on its own when done.',
updater_apply_not_configured: 'Auto-update is not enabled on this server. Ask your administrator to apply this update via SSH.',
config_error_logs: 'System errors',
error_logs_clear: 'Clear log',
@@ -1,7 +1,8 @@
import { RefreshCw } from 'lucide-react';
import { useState } from 'react';
import { RefreshCw, Download } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAsync } from '../../hooks/useAsync.js';
import { getUpdateStatus } from '../../services/updater.js';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { getUpdateStatus, applyUpdate } from '../../services/updater.js';
import { Button } from '../../components/Button.jsx';
import { Badge } from '../../components/Badge.jsx';
import { InfoBar } from '../../components/InfoBar.jsx';
@@ -9,18 +10,46 @@ import { InfoBar } from '../../components/InfoBar.jsx';
const CRITICALITY_COLOR = { alta: 'danger', media: 'warning', baja: 'success' };
export default function UpdaterPanel() {
const { t } = useUi();
const { t, openDialog, pushToast } = useUi();
const { data: status, loading, error, reload } = useAsync(getUpdateStatus, []);
const runAction = useAction();
const [applying, setApplying] = useState(false);
function confirmApply() {
openDialog({
tone: 'warning',
title: t('updater_apply_confirm_title'),
message: `${t('updater_apply_confirm_msg')} v${status.currentVersion} → v${status.latestVersion}`,
actions: [
{ label: t('common_cancel'), variant: 'standard' },
{
label: t('updater_apply_confirm_ok'), variant: 'danger',
onClick: () => runAction(async () => {
setApplying(true);
await applyUpdate();
pushToast({ tone: 'success', message: t('updater_apply_started') });
}).finally(() => setApplying(false)),
},
],
});
}
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
<Button variant="standard" icon={RefreshCw} onClick={reload} disabled={loading}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
<Button variant="standard" icon={RefreshCw} onClick={reload} disabled={loading || applying}>
{t('updater_check')}
</Button>
{status && !status.upToDate && status.applyAvailable && (
<Button variant="accent" icon={Download} onClick={confirmApply} disabled={applying}>
{applying ? t('updater_applying') : t('updater_apply')}
</Button>
)}
{status && <span className="text-muted" style={{ fontSize: '.78rem' }}>{t('updater_current_version')} v{status.currentVersion}</span>}
</div>
{applying && <InfoBar tone="warning" dismissible={false}>{t('updater_applying_hint')}</InfoBar>}
{loading && <div className="text-muted">{t('common_loading')}</div>}
{error && <InfoBar tone="error" dismissible={false}>{error}</InfoBar>}
@@ -31,6 +60,9 @@ export default function UpdaterPanel() {
{status && !status.upToDate && (
<div>
<InfoBar tone="error" dismissible={false}>{t('updater_restart_required')}</InfoBar>
{!status.applyAvailable && (
<InfoBar tone="warning" dismissible={false}>{t('updater_apply_not_configured')}</InfoBar>
)}
{status.hasMigrations && (
<InfoBar tone="warning" dismissible={false}>{t('updater_has_migrations')}</InfoBar>
)}
+1
View File
@@ -1,3 +1,4 @@
import { api } from './api.js';
export const getUpdateStatus = () => api.get('/updater/status');
export const applyUpdate = () => api.post('/updater/apply');