CreaRack-SL

Add Rack creaba dos racks — doble instancia de módulo por manifest hasheado (v1.77.3)

{“tags”: [“racks”,“frontend”,“core”,“reliability”,“data-integrity”,“django”,“javascript”,“staticfiles”,“monitoring”,“testing”], “sources”: [{“type”:“commit”,“ref”:“a5f18f998d703714aaf5e289a1202429aebc42d9”,“last_seen”:“2026-08-21”},{“type”:“code”,“ref”:“templates/row.html”,“last_seen”:“2026-08-21”},{“type”:“code”,“ref”:“static/js/rows/row_view.js”,“last_seen”:“2026-08-21”},{“type”:“code”,“ref”:“static/js/rows/row_actions.js”,“last_seen”:“2026-08-21”},{“type”:“commit”,“ref”:“6adc248816c48011674d3a922a101377bd100508”,“last_seen”:“2026-08-27”},{“type”:“code”,“ref”:“tests/monitoring/test_static_module_versions.py”,“last_seen”:“2026-08-27”},{“type”:“code”,“ref”:“static/js/pages/observatory/index.js”,“last_seen”:“2026-08-27”}], “content”: ”## Cuándo\n\n21-08-2026, detectado en producción por Edu. Fix el mismo día, commit a5f18f99 (PR #411, v1.77.3).\n\n## Síntomas visibles\n\nUn solo clic en el botón “Add Rack” de la página de una Fila (Row) creaba dos racks de golpe en producción. En local el bug no se reproducía nunca — solo se manifestaba online.\n\n## Causa raíz\n\ntemplates/row.html cargaba static/js/rows/row_actions.js como <script type=\"module\"> propio, además de que row_view.js ya lo importa internamente (import { removeRack, reorderRow } from './row_actions.js').\n\nEn desarrollo esto es inofensivo: el navegador dedupe módulos ES por URL exacta, y ambas referencias ({% static %} y el import relativo) resuelven a la misma URL sin hash.\n\nEn producción, [[entity—core—service—forgiving-manifest-storage]] (el backend de estáticos de Django) reescribe la URL del <script src=\"{% static ... %}\"> a una ruta hasheada (p. ej. row_actions.a1b2c3.js), pero no reescribe el import interno dentro de row_view.js (ese import va literal, sin pasar por {% static %}). El navegador ve dos URLs distintas para el mismo fichero lógico → carga el módulo dos veces → el listener del botón “Add Rack” queda registrado dos veces → un clic dispara dos POST.\n\n## Fix aplicado\n\nCommit a5f18f998d703714aaf5e289a1202429aebc42d9: se elimina la etiqueta <script type=\"module\" src=\"{% static 'js/rows/row_actions.js' %}\"> de templates/row.html. El import interno de row_view.js ya cablea el botón; no hace falta cargar el módulo por separado. Verificado el mecanismo directamente en PROD (ambas URLs — manifest hasheado e import sin hash — se servían a la vez).\n\n## Lecciones\n\n- Un módulo JS que se importa desde otro módulo no debe cargarse también como <script> de página en templates que usan estáticos hasheados — el dedupe por URL que funciona en local se rompe en producción porque el manifest solo reescribe las URLs que pasan por {% static %}, no los imports internos de JS.\n- El bug solo se manifiesta con el backend de estáticos hasheado activo ([[entity—core—service—forgiving-manifest-storage]]), por eso quedó invisible en desarrollo local durante todo el ciclo de esa feature.\n\n## Preventivos futuros\n\n- Al añadir un <script type=\"module\"> en un template, comprobar si ese módulo ya se importa desde otro módulo cargado en la misma página (grep del nombre de archivo en static/js/**) antes de añadir la etiqueta.\n- Resuelto 27-08-2026 (commit 6adc2488, deuda #274 Tanda B, B1-#18): la auditoría manual pendiente se sustituyó por un guardián automático. tests/monitoring/test_static_module_versions.py ya vigilaba que un mismo módulo no se importara con dos números de ?v= distintos, pero no veía el caso de este incidente: un import sin ?v= es otra URL más del mismo módulo, y el regex original lo ignoraba. Al tratar “sin ?v=” como una versión más ((sin ?v=)), el test destapó 10 módulos cargados bajo dos URLs a la vez en producción (utils/html.js, utils/TabHelper.js, utils/DeviceFilterService.js, WirelessChartRegistry.js, SignageChartRegistry.js, UpsChartRegistry.js y cuatro módulos re-exportados desde observatory/index.js). Se unificaron los 23 importadores afectados; el test ahora falla si el patrón reaparece, en vez de depender de que alguien haga el grep manual.\n\n## Véase también\n\n- [[entity—core—service—forgiving-manifest-storage]]\n- [[incident—20260503—full-backup-http500]]\n”}