fix: version 2.8.2 -- overlay de progreso al actualizar + fix mountedRef en StrictMode

Agrega un overlay de pantalla completa (circulo girando + porcentaje estimado +
"Actualizando...") que bloquea todo clic mientras corre la auto-actualizacion, y recarga la
pagina sola al terminar. De paso corrige un bug real: el useEffect de mountedRef no
reactivaba el flag en su setup, asi que con React.StrictMode (dev) el polling de estado
nunca llegaba a correr -- no afectaba produccion (StrictMode no duplica efectos ahi) pero
ocultaba el problema al probar en dev. Tambien se corrigen un par de voseos que quedaron
en los textos de Actualizaciones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-01 14:33:55 -05:00
parent 8214df62ae
commit d5f3f05470
7 changed files with 79 additions and 13 deletions
+12
View File
@@ -1,5 +1,17 @@
# Changelog - project.linkedcorp.net # Changelog - project.linkedcorp.net
## [2.8.2] - 2026-10-01
### Added
- Al aplicar una actualización desde Configuración → Actualizaciones, ahora se bloquea toda la
pantalla con un círculo girando y un porcentaje estimado en el centro ("Actualizando…") hasta
que el sistema termina de reiniciarse; al confirmar que ya está al día, la página se recarga
sola para traer los cambios.
### Fixed
- El seguimiento de la actualización (`pollUntilUpToDate`) nunca corría en el entorno de
desarrollo por un efecto de React mal planteado (`mountedRef` quedaba en `false` para siempre
por el doble-render de `React.StrictMode`) — no afectaba producción, pero sí ocultaba el bug al
probar en dev.
## [2.8.1] - 2026-10-01 ## [2.8.1] - 2026-10-01
### Added ### Added
- Catálogo de íconos de "Contraseñas" ampliado con ~70 logos de marcas conocidas (Google, GitHub, - Catálogo de íconos de "Contraseñas" ampliado con ~70 logos de marcas conocidas (Google, GitHub,
+1 -1
View File
@@ -1 +1 @@
2.8.1 2.8.2
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "project-linkedcorp-backend", "name": "project-linkedcorp-backend",
"version": "2.8.1", "version": "2.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/server.js", "main": "src/server.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "project-linkedcorp-frontend", "name": "project-linkedcorp-frontend",
"private": true, "private": true,
"version": "2.8.1", "version": "2.8.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+6 -4
View File
@@ -81,15 +81,16 @@ export const messages = {
updater_criticality_alta: 'Alta', updater_criticality_alta: 'Alta',
updater_criticality_media: 'Media', updater_criticality_media: 'Media',
updater_criticality_baja: 'Baja', 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_rollback_hint: 'Antes de actualizar, respalda la base de datos. Si algo sale mal, restaura ese respaldo y vuelve al commit anterior a esta actualización en el historial de Gitea.',
updater_apply: 'Actualizar', updater_apply: 'Actualizar',
updater_applying: 'Actualizando…', updater_applying: 'Actualizando…',
updater_apply_confirm_title: 'Aplicar actualización', 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_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_confirm_ok: 'Sí, actualizar',
updater_apply_started: 'Actualización iniciada. El sistema se reiniciará en unos minutos — refrescá la página luego.', updater_apply_started: 'Actualización iniciada. El sistema se reiniciará en unos minutos.',
updater_applying_hint: 'Actualización en curso — no cierres esta pestaña. El sistema se reiniciará solo al terminar.', 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.', updater_apply_timeout: 'La actualización está tardando más de lo esperado. Revisa el estado en unos minutos o verifica el servidor.',
updater_apply_not_configured: 'La auto-actualización no está habilitada en este servidor. Pide a tu administrador que aplique esta actualización por SSH.',
updater_login_popup_title: 'Hay una actualización pendiente', updater_login_popup_title: 'Hay una actualización pendiente',
updater_login_popup_msg: 'El sistema tiene una versión nueva disponible:', updater_login_popup_msg: 'El sistema tiene una versión nueva disponible:',
updater_login_popup_go: 'Ver actualizaciones', updater_login_popup_go: 'Ver actualizaciones',
@@ -417,8 +418,9 @@ export const messages = {
updater_apply_confirm_title: 'Apply update', 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_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_confirm_ok: 'Yes, update',
updater_apply_started: 'Update started. The system will restart in a few minutes — refresh the page afterward.', updater_apply_started: 'Update started. The system will restart in a few minutes.',
updater_applying_hint: 'Update in progress — do not close this tab. The system will restart on its own when done.', updater_applying_hint: 'Update in progress — do not close this tab. The system will restart on its own when done.',
updater_apply_timeout: 'The update is taking longer than expected. Check back in a few minutes or verify the server.',
updater_apply_not_configured: 'Auto-update is not enabled on this server. Ask your administrator to apply this update via SSH.', updater_apply_not_configured: 'Auto-update is not enabled on this server. Ask your administrator to apply this update via SSH.',
updater_login_popup_title: 'There is a pending update', updater_login_popup_title: 'There is a pending update',
updater_login_popup_msg: 'A new version of the system is available:', updater_login_popup_msg: 'A new version of the system is available:',
@@ -16,27 +16,44 @@ export default function UpdaterPanel() {
const { data: status, loading, error, reload, setData } = useAsync(getUpdateStatus, []); const { data: status, loading, error, reload, setData } = useAsync(getUpdateStatus, []);
const runAction = useAction(); const runAction = useAction();
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [progressPct, setProgressPct] = useState(0);
const mountedRef = useRef(true); const mountedRef = useRef(true);
useEffect(() => () => { mountedRef.current = false; }, []); // Setup explicito (no solo el cleanup): con React.StrictMode en dev el efecto se dispara
// mount->cleanup->mount de una, y si el setup no reactiva el flag queda en false para siempre
// aunque el componente SI este montado -- pollUntilUpToDate nunca correria.
useEffect(() => {
mountedRef.current = true;
return () => { mountedRef.current = false; };
}, []);
// El servidor se reinicia a mitad de la actualizacion (backend y frontend), asi que durante // El servidor se reinicia a mitad de la actualizacion (backend y frontend), asi que durante
// ese rato las llamadas van a fallar por un momento -- se ignoran y se sigue insistiendo. // ese rato las llamadas van a fallar por un momento -- se ignoran y se sigue insistiendo. El
// Cuando confirma que ya esta al dia, el boton "Actualizar" desaparece solo (esta gateado por // porcentaje es una estimacion sobre el tope de espera (no hay progreso real del script), pero
// !status.upToDate mas abajo) sin depender de que la pestaña se recargue. // alcanza para que la persona sepa que sigue avanzando y no quede mirando una pantalla muerta.
// Al confirmar que ya esta al dia, se recarga la pestaña sola para traer el build nuevo.
async function pollUntilUpToDate() { async function pollUntilUpToDate() {
for (let i = 0; i < POLL_MAX_ATTEMPTS && mountedRef.current; i++) { for (let i = 0; i < POLL_MAX_ATTEMPTS && mountedRef.current; i++) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
if (!mountedRef.current) return; if (!mountedRef.current) return;
setProgressPct(Math.min(95, Math.round(((i + 1) / POLL_MAX_ATTEMPTS) * 100)));
try { try {
const result = await getUpdateStatus(); const result = await getUpdateStatus();
if (!mountedRef.current) return; if (!mountedRef.current) return;
setData(result); setData(result);
if (result.upToDate) { setApplying(false); return; } if (result.upToDate) {
setProgressPct(100);
await new Promise((resolve) => setTimeout(resolve, 600));
window.location.reload();
return;
}
} catch { } catch {
// backend reiniciandose -- se ignora y se reintenta en el proximo ciclo // backend reiniciandose -- se ignora y se reintenta en el proximo ciclo
} }
} }
if (mountedRef.current) setApplying(false); if (mountedRef.current) {
setApplying(false);
pushToast({ tone: 'warning', message: t('updater_apply_timeout') });
}
} }
function confirmApply() { function confirmApply() {
@@ -49,6 +66,7 @@ export default function UpdaterPanel() {
{ {
label: t('updater_apply_confirm_ok'), variant: 'danger', label: t('updater_apply_confirm_ok'), variant: 'danger',
onClick: () => runAction(async () => { onClick: () => runAction(async () => {
setProgressPct(0);
setApplying(true); setApplying(true);
await applyUpdate(); await applyUpdate();
pushToast({ tone: 'success', message: t('updater_apply_started') }); pushToast({ tone: 'success', message: t('updater_apply_started') });
@@ -60,6 +78,18 @@ export default function UpdaterPanel() {
return ( return (
<div> <div>
{applying && (
<div className="update-overlay" role="alert" aria-live="assertive">
<div className="update-spinner-wrap">
<div className="update-spinner-ring" />
<div className="update-spinner-center">
<div className="update-spinner-label">{t('updater_applying')}</div>
<div className="update-spinner-pct">{progressPct}%</div>
</div>
</div>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
<Button variant="standard" icon={RefreshCw} onClick={reload} disabled={loading || applying}> <Button variant="standard" icon={RefreshCw} onClick={reload} disabled={loading || applying}>
{t('updater_check')} {t('updater_check')}
+22
View File
@@ -390,6 +390,28 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
.vault-icon-option.brand, .vault-card-icon.brand { background: #fff; } .vault-icon-option.brand, .vault-card-icon.brand { background: #fff; }
.vault-icon-option.brand.selected { border-color: rgb(var(--accent)); } .vault-icon-option.brand.selected { border-color: rgb(var(--accent)); }
/* --- Overlay de actualizacion (ver UpdaterPanel.jsx) ---
Bloquea toda la pantalla (no solo el panel) mientras corre scripts/apply-update.sh, porque el
backend y el frontend se reinician a mitad de proceso -- cualquier clic en ese rato no tiene
adonde ir. z-index por encima de dialog-overlay/detail-popup-overlay a proposito. */
.update-overlay {
position: fixed; inset: 0; background: rgb(0 0 0 / .7); z-index: 300;
display: flex; align-items: center; justify-content: center;
}
.update-spinner-wrap { position: relative; width: 150px; height: 150px; flex: none; }
.update-spinner-ring {
position: absolute; inset: 0; border-radius: 999px;
border: 6px solid rgb(255 255 255 / .2); border-top-color: rgb(var(--accent));
animation: update-spin 0.9s linear infinite;
}
.update-spinner-center {
position: absolute; inset: 0; display: flex; flex-direction: column; gap: 2px;
align-items: center; justify-content: center; text-align: center; color: #fff;
}
.update-spinner-label { font-size: .78rem; font-weight: 600; opacity: .85; }
.update-spinner-pct { font-size: 1.7rem; font-weight: 700; }
@keyframes update-spin { to { transform: rotate(360deg); } }
/* --- DetailPopup (ver DetailPopup.jsx) --- /* --- DetailPopup (ver DetailPopup.jsx) ---
Al elegir un item de un listado, la lista queda inactiva detras del overlay y el popup Al elegir un item de un listado, la lista queda inactiva detras del overlay y el popup
presenta la pantalla de info/edicion completa. Regla dura: NUNCA se cierra con clic afuera presenta la pantalla de info/edicion completa. Regla dura: NUNCA se cierra con clic afuera