basquetWi + New ticket
pluto PLUTO-718

togglePeriodoActiveRoster clears isCurrent/isActive on EVERY periodo with no server-side guard - a direct call leaves the app with no current periodo

Done normal cpcoder-pluto-cc

Found while fixing PLUTO-136 (3), filed rather than folded in because the fix is a design choice, not a guard. src/lib/administrar-actions.ts togglePeriodoActiveRoster does: UPDATE periodos SET "isCurrent" = false, "isActive" = false WHERE "deletedAt" IS NULL IF NOT wasCurrent THEN UPDATE ... SET isCurrent = true, isActive = true WHERE id = $1 So when the target WAS current, the second statement does not run and the transaction commits with every periodo deactivated. Everything scoped to the current periodo - rosters, /informes, admin-batch.ts:382 and admin-actions.ts:1917 which both do 'SELECT id FROM vPeriodos WHERE isCurrent = true ... LIMIT 1' - then resolves to nothing. PLUTO-136 added a confirm dialog on the deactivating direction, which is an AFFORDANCE. The server action itself is still reachable directly and still empties the column. The dialog makes the accident unlikely; it does not make the state unreachable. Open design question, which is why this is not just 'add a guard': is 'no current periodo' a legal state? If it is legal (between academic years, say) then nothing is broken and this WI closes as by-design with the readers documented. If it is NOT legal, the fix is a server-side refusal to deactivate the last current periodo, and probably a partial unique index or a CHECK making the invariant structural rather than procedural - db-pluto-cc lane. Do not silently add a refusal without answering that first: refusing the toggle would block the legitimate end-of-year case if it exists.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 7w ago
  • wi-cli-venus note · 7w ago
    Observability half shipped 2bc9d47 (v2.23.77, live-verified: /api/health reports 2.23.77 = remote main package.json, deploymentId dpl_EeDPpNVZjKdYUUUTiHUPwphjUmq9). Both paths that can empty the column - togglePeriodoActiveRoster deactivating direction, and removePeriodo on the current periodo, whose softDelete extraSet clears isCurrent/isActive and is NOT stopped by the child-row block since a periodo can be made current without acquiring children - now emit a warn. The check counts the POST-COMMIT state rather than inferring it from wasCurrent, so one call covers every present and future path; it runs after COMMIT and best-effort, because a logging fault must not report a committed roster change as failed. 5 behavioural tests against a fake pg client, mutation-checked three ways (both calls disabled = 3 fail; count-guard disabled = the silent-case test fails; the ordering test pins the count after COMMIT). Also removed three dead imports the file carried (requireRoleForAction, logInfo, sendAdminEmail). MEASUREMENT that changes the WI premise: the state is NOT corrupt - four consumers already model it with explicit Spanish errors (practica-actions:153 "No hay un periodo activo", periodoNotCurrentError, the approve-membership refusal in admin-actions:1935, /administrar/layout falling back to periodos[0]), so a blanket refusal would block a legitimate end-of-year close. Five OTHER consumers were degrading silently and that is what the warn addresses: weekly KPI cron returns all-zeros and mails nobody (indistinguishable from a quiet week), the alumno gate in (protected)/layout stops redirecting to /completar-asignacion because its HAVING runs over zero rows, admin-batch:382 and approve-request skip the student assignment, /administrar shows a stale default. Note togglePeriodoActive in admin-actions is NOT a toggle and cannot produce the state - it sets isCurrent = (p.id = target) for every row. WI stays open on the design half: eq#60 (topic periodo-lifecycle) asks Elazar whether no-current-periodo is legal (A: keep it legal, decide whether the five silent consumers should say so on screen) or illegal (B: server refusal + a partial unique index, db-pluto-cc lane). Do not add the refusal before that answer.
  • wi-cli-venus statusChanged · 7w ago
    Observability shipped; design half held on eq#60
  • wi-cli-venus statusChanged · 5w ago
    eq#57 RESUELTA A (rev 216, coder-pluto-cc bajo el grant acotado, NO son palabras de Elazar): el estado sin periodo vigente ES legal, no se agrega rechazo de servidor ni se toca la base. Queda SOLO hacer visible lo que hoy se degrada en silencio. INVENTARIO DE MENSAJES medido 2026-09-11 (para no inventar una tercera redaccion): ~14 paginas YA dicen 'No hay un periodo activo' (/administrar/*, /practicas, /practicas/nueva, /alumnos, /mi-cursada, /informes, /equipo-academico). periodoNotCurrentError (administrar-actions:83) dice 'No hay un periodo vigente'. practica-actions:173 dice 'No hay un periodo activo' sin tilde. admin-actions:1943 dice 'No hay un periodo activo para registrar la asignacion.'. CONCLUSION: las pantallas de LECTURA ya estan cubiertas; el hueco son los caminos de ESCRITURA, el cron y el gate. LOS CINCO SITIOS, con lo que hay que hacer en cada uno: 1) src/app/api/cron/practicas-kpi-report/route.ts:233 - runReport() devuelve {sent:0,failed:0,skipped:0} y no manda nada; indistinguible de una semana tranquila. FIX: mandar el informe igual con cuerpo explicito 'sin periodo vigente' (el mail ES la pantalla de un cron). Registrar info, no warn: 2bc9d47 ya escribe un warn al ENTRAR al estado, y un warn semanal durante dos meses de receso son ~8 paginas contra la linea de base de cero ruido. 2) src/app/(protected)/layout.tsx:16-32 - el gate de alumno deja de redirigir porque el HAVING corre sobre cero filas. NO agregar una segunda query: la actual ya pega a vPeriodos en cada render (~20 renders concurrentes por vista, eq#87). Reestructurar ESA query para que discrimine 'no hay periodo vigente' de 'hay periodo y el alumno no tiene asignacion'. MEDIDO: /completar-asignacion NO sirve sin periodo vigente - fn_getStudentAssignmentBootstrap devuelve periodoId y comisiones, y sin periodo la lista viene vacia, o sea que redirigir ahi dejaria al alumno en un formulario sin opciones. Entonces no-periodo NO redirige (el comportamiento actual es el correcto) y lo que falta es el aviso; solo sin-asignacion redirige. 3) src/lib/admin-batch.ts:379-392 - el alta de usuario saltea el INSERT en studentAssignments dentro de un if (rows.length > 0) sin rama else. Devuelve {ok:true,id}. FIX: devolver tambien un warning y RENDERIZARLO; un warning en el valor de retorno que nadie muestra es un sexto camino silencioso, no un arreglo. 4) src/lib/admin-actions.ts:1935-1943 - writeApprovalMembership devuelve {ok:true} pelado para studentAssignments cuando no hay periodo (las membresias docentes ya fallan fuerte con mensaje). Cadena: writeApprovalMembership -> approveAccessRequest (linea 2027, ActionResult<{emailSent?:boolean}>) -> componente cliente. FIX: warning que suba por los tres niveles hasta el DOM. 5) /administrar muestra el periodo mas reciente por defecto sin avisar. Los periodos ya estan cargados ahi, el cartel no cuesta una query. REGLA PARA 3 y 4: trazar hasta el componente que renderiza el resultado y testear el RENDER, no solo el valor devuelto.
  • wi-cli-venus completed · 5w ago
    SHIPPED a21a70f, v2.23.90, 2026-09-11 11:31 ART. Option A de eq#57 (rev 216, decidida por coder-pluto-cc bajo el permiso acotado de auto-decision; NO son palabras de Elazar, revocable como fila suelta): cero periodo vigente es un estado LEGAL, asi que no se agrego rechazo del lado del servidor ni se toco la base. La premisa de pmmaster (togglePeriodoActiveRoster deja la app sin periodo y eso rompe la app para todos, es un defecto vivo) quedo corregida con la medicion que ya estaba en la fila y aceptada a las 11:13.\n\nLO QUE CAMBIO, cinco sitios que degradaban en silencio: (1) banner ambar role=status en el layout protegido, leyendo el periodo por getActivePeriodo (envuelto en cache() de React, comparte la consulta con cada pagina que ya lo lee, cero costo de pool extra); el llamado a la accion esta gateado por la capacidad managePeriodos, o sea que a un alumno se le informa el estado y NO se le dice que active un periodo desde Administrar, instruccion que no puede seguir. (2) el cron semanal de KPI devuelve reason noCurrentPeriodo en su fila de corrida, que ya iba con forceSync; deliberadamente NO es un mail, porque las tres listas de destinatarios estan keyed en periodoId y sin periodo no hay a quien escribirle. (3) approveAccessRequest devuelve advertencia en vez de ok pelado. (4) createUserFromForm idem via admin-batch. (5) las dos pantallas de admin la RENDERIZAN (linea de resultado y toast con duration Infinity). El codigo de friccion nuevo quedo registrado en el set cerrado de PLUTO-534.\n\nNO SE TOCO, y quedo fijado con un test negativo: el gate de alumno del layout no se gatea por activePeriodo, porque redirigir a /completar-asignacion sin periodo deja al alumno en un formulario con lista vacia. Tambien fijado: el Aprobado masivo de access-requests-list no lleva advertencia porque ese camino es el batch de CAMBIO DE MAIL (fn_approveEmailChangesBatch, migracion 090, cero referencias a studentAssignments) y nunca intenta una asignacion.\n\nCOBERTURA: 52 tests en dos archivos (3 ejecutables nuevos en admin-batch.test.ts mas un guard a nivel fuente sobre los siete archivos con piso anti-vacio), mutation-checked dos veces: blanquear la advertencia de admin-batch rompe exactamente ese caso, borrar el banner rompe exactamente el caso (1) de 8. NO EJERCITADO, dicho para que la afirmacion sea honesta: no hay render en navegador, no hay Postgres, no corrio el cron, y no se asegura que el toast sobreviva al router.push.
bug
7w ago by wi-cli-venus
5w ago
2026-09-11 14:33