Un deploy mientras alguien carga un alta le cuesta todo lo tipeado: no hay persistencia de borrador
REPORTE REAL, no hipotetico. Eugenia Grosclaude, 2026-09-07 16:05 ART, sobre Leandro cargando un alta de movil: "La aplicacion se actualizo y esta pestaña quedo con una version anterior. Para no perder lo que escribiste, copialo antes de recargar la pagina." ID de solicitud: c1713527-2cab-4a04-b7b0-f68abbf94b9b Fecha: 2026-09-07T19:03:04.035Z (16:03:04 ART) URL: https://dev01.movilba.com/vehiculos/agregar/colectivo Version: 5.5.47 EL CARTEL NO ES EL DEFECTO - es MOVILBA-247 funcionando bien. `bundle-desactualizado.ts` detecta el "Server action not found" que deja una pestaña abierta antes de un deploy, y como `hayEdicionSinGuardar` era true se NEGO a recargar sola para no tirarle lo tipeado, y le mostro el mensaje con la advertencia de copiar. Correcto de punta a punta. EL DEFECTO ES QUE NO HAY A DONDE VOLVER. La unica opcion que le queda al usuario es copiar a mano un formulario largo -el alta de colectivo- antes de recargar. Medido: `grep localStorage|sessionStorage` sobre src/app/vehiculos/agregar/ y src/components/ no devuelve NADA de borrador. Lo unico que sobrevive a una recarga hoy es el TEMA (ThemeToggle) y un dismiss de aviso de password. **Se persiste la preferencia de color y no se persiste un alta a medio cargar.** CAUSA DE FONDO, y es nuestra: se deployo encima de un usuario que estaba trabajando. Medido en journalctl -u dev-movilba-web: **30 eventos Stopped/Started entre 15:00 y 16:15**, o sea ~15 reinicios. Los que rodean el caso de Leandro son 16:02:11 y 16:03:49; su cartel es de 16:03:04, o sea que lo invalido el deploy de 16:02:11. Ese no fue mio -los mios empiezan 16:07- pero la CLASE es "deployar mientras alguien usa la app" y yo puse cinco reinicios en esa clase entre 16:07 y 16:16. No me absuelvo por el intervalo: esa es exactamente la trampa de reference_un_push_de_solo_documentacion_reinicia_dev01_igual_que_uno_de_codigo. ARREGLO, dos mitades independientes: 1. PRODUCTO: persistir el borrador del formulario en sessionStorage por pantalla, y restaurarlo despues de la recarga. Sin eso, todo deploy sigue costandole a quien este cargando. Ojo con que el borrador NO se convierta en un lugar donde queden datos personales despues de un logout - se limpia al enviar bien y al cerrar sesion. 2. PROCESO: no deployar a dev01 mientras haya alguien cargando. Hoy no hay forma de SABERLO desde la consola, que es la mitad que falta - la respuesta a "cuando puede un usuario cargar un colectivo en dev" hoy es "cuando ningun carril este deployando", y eso no esta coordinado por ningun lado. El lock de dev01-deploy existe (hub /lock) pero protege contra otro carril, no contra un usuario humano que no puede tomarlo. NO ES un problema de red, ni de auth, ni de la sesion de Leandro. La fecha en UTC del cartel (19:03:04.035Z) y ART (16:03:04) es la misma hora, ya convertida.
Sub-tickets — 1/1 done
Activity
-
2026-09-07 19:32 - DOS INCIDENTES, NO UNO, y una correccion mia que casi los entierra a los dos. ERROR MIO: le dije a db02-mba-l-cc que NO buscara la fila de c1713527 porque 'es un cartel renderizado en el cliente y MOVILBA-329 dice que no va a tener fila'. Al reves. 329 es justamente el arreglo que hizo que ESTO se loguee: logClientError -> /api/log-error. Cite el ticket que prueba que la fila EXISTE como si dijera que no existe. db02 la busco igual. LA FILA (app_logs, match exacto por request_id): - 16:03:04.142518 - web-client - warn - 'No se pudo crear el colectivo' - request_id c1713527-2cab-4a04-b7b0-f68abbf94b9b - data: url /vehiculos/agregar/colectivo - error 'Server action not found.' - stack en _next/static/chunks/3lkrhlneun9c4.js:2:743 - source 'client' - timestamp 2026-09-07T19:03:04.033Z - userAgent Chrome/152 Windows - status_code y error_code NULL, como en todas las filas de DEV. SEGUNDO INCIDENTE, no reportado hasta ahora: 15:50:55.547 - web-client - warn - mismo mensaje, mismo 'Server action not found.', mismo chunk 3lkrhlneun9c4.js, requestId d9fcdd43-0c12-403b-adda-254aa57d3f39, misma maquina. **Leandro perdio la carga DOS VECES en 12 minutos y solo una llego a Eugenia.** CONSECUENCIAS: 1. El denominador de este item es 2 incidentes, no 1. Y el que no se reporto es el dato importante: lo que llega por WhatsApp es una MUESTRA de lo que pasa, no el conteo. Lo unico que hace contable a esto es la fila, no el reporte. 2. MOVILBA-329 deja de estar 'shipeado sin probar' para este camino: un requestId que un usuario REAL leyo de un cartel resuelve a una fila real generada por un navegador en un incidente real. Es exactamente el par pantalla-fila que faltaba a la mañana, y lo produjo un usuario, no una sonda. 3. La linea de tiempo queda corroborada por dos instrumentos independientes: el timestamp del cliente (19:03:04.033Z) y el de escritura del servidor (16:03:04.142 ART) difieren 109ms, asi que el reloj del navegador y el nuestro coinciden y el incidente cae entre los reinicios de 16:02:11 y 16:03:49. Medido por db02-mba-l-cc.
-
2026-09-07 19:34 - DENOMINADOR REAL, y corrige de nuevo el encuadre. Medido por db02-mba-l-cc sobre DEV a las 19:31, por data->>'error' = 'Server action not found.', re-derivable: TOTAL: 106 filas, 106 requestIds distintos, 22 URLs distintas, desde 2026-08-20 12:24:10 hasta hoy 16:03:04. Ampliando a cualquier fallo de chunk/bundle: 122/122. Una fila por requestId, sin duplicados que inflen. POR DIA: 08-20 **44** - 08-25 3 - 08-26 14 - 08-27 1 - 09-03 1 - 09-04 **26** - 09-05 12 - 09-06 1 - **09-07 4**. 1. HOY SON 4, NO 44. La tormenta de deploys de esta tarde NO es el peor dia del registro: 08-20 y 09-04 lo son. Yo estaba por escribir que esto era 'lo mas caro que hicimos hoy' y habria estado mal en las dos direcciones - subestima la historia y sobreestima el dia. Esto es una CONDICION PERMANENTE, no una consecuencia de esta tarde. 2. LA POBLACION DE ESTE ITEM NO ES 106: son las ~10 filas donde alguien estaba TIPEANDO. Las URLs con /agregar/colectivo o /editar/colectivo son las 2 de hoy mas 09-03, 09-04 (x4) y 09-05 (x3). Todo el resto pega en pantallas de detalle o de lista, donde recargar no cuesta nada. Ese es el numero que va en el item. 3. SOLO 2 de las 4 de hoy son de Leandro, y son las unicas con level=warn y 'No se pudo crear el colectivo'. Las otras 102 son level=error y 'Unhandled Promise Rejection: Server action not found.' La distincion es la de MOVILBA-329: un rejection no manejado llega a onunhandledrejection y se lleva la auto-recuperacion; uno CAPTURADO -que es lo que produce el formulario de alta- se lleva el cartel y ninguna recuperacion. **Leandro cae en la rama capturada JUSTAMENTE PORQUE esa pantalla maneja bien sus errores.** Es la trampa ya escrita de 'las pantallas mejor escritas se auto-excluyen del arreglo'. 4. LA CLAVE ES data->>'error', no message. El message cambia segun la rama, asi que un grep por mensaje encuentra una rama y se pierde la otra - por eso 102 de estas quedaron sin contar hasta esta noche.
-
coder03-mba-l-cc / coder
-
coder04-mba-l-ca
-
researcher-mba-l-cc / solo
-
Researcher started at 2026-10-08T11:59:41-03:00.
-
researcher-mba-l-cc TODAY'S BEHAVIOR (confirmed by code, matches the ticket's own measurement): grep for localStorage|sessionStorage under src/app/vehiculos/agregar/ and src/components/ returns nothing draft-related. src/lib/bundle-desactualizado.ts already tracks "is there unsaved edits in this tab" via a global hayEdicionSinGuardar flag, set on any input/select/textarea event (excluding data-sin-guardar-marked search/filter controls), cleared only on route change (limpiarEdicion, called by NotificationProvider on pathname change). That flag already decides whether to auto-reload (MOVILBA-247) vs show the "copy it by hand" message - but it is a boolean, not a snapshot: nothing captures the actual typed values, so the only thing surviving reload is the fact that something was unsaved, not what it was. STORAGE: sessionStorage, not localStorage. Per-screen, per-tab draft that should NOT survive closing the browser (ticket's own logout-hygiene concern) and should NOT leak across tabs mid-edit (localStorage would let a second tab silently clobber or race the draft). SCOPE for this ticket: src/app/vehiculos/agregar/colectivo/page.tsx first (ColectivoFormData - 24 fields, all vehicle/technical: dominio, serial numbers, catalog ids, expediente - no persona/CUIT data on this specific form, so the PII-at-logout concern is lower here than on an entidad/persona alta, but still apply the same clear-on-logout rule since the pattern will extend to those forms next). KEY: `movilba.borrador.<ruta>` (e.g. `movilba.borrador.vehiculos/agregar/colectivo`) - route-scoped, not global, so two different add-forms in two tabs don't collide. WRITE: debounced (e.g. 400ms) on the same input listener bundle-desactualizado.ts already attaches, writing `JSON.stringify(formData)` - reuses the existing dirty-detection wiring instead of adding a second listener. RESTORE: on mount, read the key for the current route; if present, hydrate formData from it instead of the empty initial state, and show a small inline notice ("se restauro un borrador sin guardar") so the user knows why fields are pre-filled instead of being confused or suspecting a stale cache. EXPIRY / CLEAR (all removeItem on the same key): 1. Successful submit (the create action resolves) - a saved alta has no reason to keep a draft. 2. Explicit cancel / navigate-away via the app's own nav (limpiarEdicion's pathname-change hook is the natural place to also clear the draft key for the route being left). 3. Logout - add to whatever already runs on sign-out (check src/lib/auth.ts signOut path) so a draft never survives past a session boundary, matching the ticket's stated requirement. 4. Age-based fallback: stamp a `savedAt` alongside the data and ignore/clear drafts older than a fixed window (e.g. 24h) on restore-read, in case step 1-3 are ever missed (e.g. tab closed without any of those firing) - sessionStorage already dies with the tab/session, so this is a belt-and-suspenders bound, not the primary mechanism. WHAT THIS DOES NOT FIX: the PROCESO half of the ticket (no deploy coordination signal for "someone is mid-form in dev01") is out of scope for a product spec - that is a process/tooling gap (the hub /lock protects against other lanes, not against a human browser tab) and needs its own decision, not a code change on this ticket. NEXT: once this lands on the colectivo add form, the same three primitives (route-scoped sessionStorage key, debounced write on the existing dirty-listener, clear on submit/nav/logout) generalize to every other agregar/* and editar/* form - no form-specific logic beyond the field shape.
-
coder04-mba-l-ca / coder
-
Scoped draft implementation in shared tree: sessionStorage key per route/tab, 24h expiry, string-field validation, 400ms debounced save on actual form changes, restore notice on reload, clear on successful create, SPA navigation, federated logout and refresh-token signout. Helper regression covers isolation/expiry/clear. Six paths are mine; other dirty paths belong to other lanes. git diff --check clean; audit requested. Process half (deploy coordination with human users) remains separate.
-
audit fail: 6 named paths (form-drafts.ts + its test, agregar/colectivo/page.tsx, ErrorContext.tsx, federated-logout.ts, SessionProvider.tsx). Gates: typecheck, eslint on the 6, test:source, test:tools. The draft lifecycle itself is UNREVIEWED - the bundle does not build, so no behaviour was exercised
payload
{"scope":"6 named paths (form-drafts.ts + its test, agregar/colectivo/page.tsx, ErrorContext.tsx, federated-logout.ts, SessionProvider.tsx). Gates: typecheck, eslint on the 6, test:source, test:tools. The draft lifecycle itself is UNREVIEWED - the bundle does not build, so no behaviour was exercised","outcome":"fail","evidence":"pnpm test:tools 97/113. All 16 failures are this diff, one cause: esbuild reports 'No matching export in boundary:@/lib/form-drafts for import clearFormDraft' at src/contexts/ErrorContext.tsx:7. clearFormDraft IS exported (src/lib/form-drafts.ts:42), so nothing is missing from the module - tools/tests/colectivo-edit-load.test.cjs loads ErrorContext.tsx for REAL via its 'real' map at :86-91 and stubs every other import from the inline mocks string, which has no form-drafts exports. typecheck and eslint are clean, which is why this only surfaces in test:tools.","reportRef":null} -
audit 2026-10-08 12:37 ART FAIL, test harness only - no defect found in the feature code, because none of it could run. CAUSE, exactly: tools/tests/colectivo-edit-load.test.cjs builds the page with an esbuild plugin (:84-95) that resolves a small 'real' allowlist (:86-91: @/utils/error-handling, @/contexts/ErrorContext, @/components/ErrorModal, @/lib/bundle-desactualizado) to real source and sends EVERYTHING else to the 'boundary' namespace, whose contents are the inline mocks string. This diff makes the real ErrorContext.tsx import clearFormDraft from @/lib/form-drafts, which lands in that namespace with no such export, so the bundle fails to build and all 16 tests in the file fail before any assertion runs. TWO WAYS TO FIX, I recommend the first: add '@/lib/form-drafts': 'src/lib/form-drafts.ts' to the real map. The test already runs in a Chromium page, so sessionStorage exists and the draft code would actually be exercised rather than stubbed - this ticket's whole point is lifecycle behaviour. Otherwise add 'export const clearFormDraft = noop; export const clearAllFormDrafts = noop; export const readFormDraft = () => null; export const saveFormDraft = noop;' to the mocks string, which unblocks the build but leaves the lifecycle untested there. Note the diff adds clearAllFormDrafts imports in SessionProvider.tsx and federated-logout.ts too, so stub all four names if you take the second route. tools/tests/colectivo-save-errors.test.cjs has its own separate stub map and does not load ErrorContext for real, so it is unaffected and currently passes. WHAT IS STILL UNREVIEWED and will be, on resubmit: the whole draft lifecycle you asked me to check - route and tab scoping, the 24h expiry, the debounce, the restore notice, and clearing on success, navigation and logout. I ran no behavioural assertion on any of it, because the one test file that renders the page never got a bundle. Do not read this FAIL as covering the feature. typecheck clean and eslint clean on the six paths, so nothing else is owed before the stub fix.
-
Audit FAIL identified only a test-harness bundle boundary: colectivo-edit-load.test.cjs loaded ErrorContext real but routed new @/lib/form-drafts to its stub namespace without exports. Added the helper to the harness real-module map so browser tests build and execute the real helper. Feature code unchanged. git diff --check clean; full lifecycle re-review requested.
-
audit pass: 7-path draft diff (form-drafts.ts/.test.ts, colectivo/page.tsx, ErrorContext.tsx, federated-logout.ts, SessionProvider.tsx, colectivo-edit-load.test.cjs); full lifecycle read. SIN EJERCITAR: browser restore/reload, logout clear and the ErrorContext clear - no test covers the page or the clears.
payload
{"scope":"7-path draft diff (form-drafts.ts/.test.ts, colectivo/page.tsx, ErrorContext.tsx, federated-logout.ts, SessionProvider.tsx, colectivo-edit-load.test.cjs); full lifecycle read. SIN EJERCITAR: browser restore/reload, logout clear and the ErrorContext clear - no test covers the page or the clears.","outcome":"pass","evidence":"typecheck clean; eslint clean on the 7 paths; test:source 65/65 incl form-drafts.test.ts (miss, roundtrip + route isolation, 24h expiry self-clears, clearFormDraft, clearAllFormDrafts prefix-only); test:tools 113/113 - colectivo-edit-load green via the real-map entry at colectivo-edit-load.test.cjs:88-92.","reportRef":null} -
audit-mba-l-cc 2026-10-08 ~13:55 ART - PASS, five non-blocking notes on the lifecycle. 1. Route+tab scoping holds. keyFor strips leading slashes, so DRAFT_PATH and usePathname() produce the same key; sessionStorage gives per-tab isolation for free. Restore is fail-closed: readFormDraft throws and self-clears unless savedAt is a number, not in the future, inside 24h, and EVERY key of empty is present as a string. Every storage access is try/caught, so private browsing or a full quota cannot block the form or the logout. 2. draftTouched is set only in handleInputChange (page.tsx:292) and handleDominioChange (:319). Checked all ~35 onChange bindings on the form - every one routes through those two, so no user-reachable edit path misses the flag. The only other setFormData sites are the restore (:117) and the identidad default (:198), and the latter is guarded by prev.identidadId ? prev : ... so it cannot overwrite a restored value. 3. ErrorContext clears the PREVIOUS pathname's draft on every pathname change. Consequence worth stating plainly: a draft survives a reload of the same route and NEVER a navigate-away-and-back. That matches the stated design, but it also means the 24h expiry and clearAllFormDrafts on logout are nearly unreachable in practice - a draft can only outlive its page via a reload, which keeps the same pathname and therefore fires no clear. Not a defect; dead-ish surface. 4. readFormDraft runs in a mount effect, so the form paints empty once and then swaps to the restored values. draftReady gates the save, not the render. Minor flash of empty fields on restore. 5. restoredDraft never resets, so the role=status notice stays visible after the user edits or after a failed submit. Cosmetic. PII note: the draft holds the whole form, dominio and expediente included, in sessionStorage. Per-tab and cleared on navigation/logout, so bounded, but it is the first time form content is persisted client-side in this app - flagging for awareness, not as an objection. SIN EJERCITAR: no test drives the page, the ErrorContext clear or the logout clear. form-drafts.test.ts covers the helper only (5 assertions over a Map-backed Storage stub). The restore/reload path has not been observed in a browser.
-
Done. Audited PASS on seven-path draft persistence diff. PM confirms shipped/live by v5.6.194/v5.6.192 on PROD, DEV and pod. Browser restore/reload and logout clear were not exercised in the audit; MOVILBA-733 separately handles navigation draft lifecycle.