[FIX] Organigramma: guardia anti-ciclo (client+server) — "Nuovo Ruolo" bloccava il sito

Segnalazione Simon: in Struttura interna/Organigramma il clic su "Nuovo Ruolo"
blocca l'intero sito. Causa: né il tree builder server (OrgRoleController::buildTree,
$attach) né il render client (organigramma.js orgNodeHtml) avevano una guardia
anti-ciclo su parent_role_id → con una gerarchia ciclica la ricorsione va in loop
infinito (hang del backend / freeze del render).
- organigramma.js orgNodeHtml(node, seen): salta gli id già visti (rompe il ciclo);
  orgRender passa un Set condiviso. Live via bind-mount (buster 20260806).
- OrgRoleController::buildTree: $attach porta la catena antenati e non ridiscende su
  un id già visto. ⚠️ Richiede reload php-fpm host (opcache) per andare live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-08-25 18:00:08 +02:00
co-authored by Claude Opus 4.8
parent 0bf17db50a
commit bfda75b292
3 changed files with 24 additions and 8 deletions
+15 -4
View File
@@ -316,11 +316,22 @@ class OrgRoleController extends BaseController
$roots[] = $r;
}
}
$attach = function (array $node) use (&$attach, $childrenByParent) {
$kids = $childrenByParent[(int) $node['id']] ?? [];
$node['children'] = array_map($attach, $kids);
// Guardia anti-ciclo: se parent_role_id formasse un ciclo (dato legacy/corrotto),
// la ricorsione andrebbe in loop infinito → hang del backend e "blocco" della pagina.
// Portiamo la catena di antenati e non ridiscendiamo su un id già visto.
$attach = function (array $node, array $ancestry) use (&$attach, $childrenByParent) {
$id = (int) $node['id'];
$ancestry[$id] = true;
$children = [];
foreach ($childrenByParent[$id] ?? [] as $kid) {
if (isset($ancestry[(int) $kid['id']])) {
continue; // ciclo: interrompi questo ramo invece di ricorrere all'infinito
}
$children[] = $attach($kid, $ancestry);
}
$node['children'] = $children;
return $node;
};
return array_map($attach, $roots);
return array_map(static fn(array $r) => $attach($r, []), $roots);
}
}
+8 -3
View File
@@ -61,10 +61,15 @@ function orgRender() {
el('org-stat-vacant').textContent = vacant;
stats.style.display = 'flex';
tree.innerHTML = ORG.tree.map(orgNodeHtml).join('');
tree.innerHTML = ORG.tree.map(n => orgNodeHtml(n, new Set())).join('');
}
function orgNodeHtml(node) {
function orgNodeHtml(node, seen) {
// Guardia anti-ciclo: se un id ricompare nella catena il render si fermerebbe in loop
// infinito (il "blocco del sito" segnalato). Rompiamo il ciclo invece di ricorrere.
seen = seen || new Set();
if (!node || seen.has(node.id)) return '';
seen.add(node.id);
const gov = node.is_governance_body;
const holder = node.holder_name
? '<div class="org-holder">👤 ' + esc(node.holder_name) + '</div>'
@@ -72,7 +77,7 @@ function orgNodeHtml(node) {
const desc = node.description ? '<div class="org-desc">' + esc(node.description) + '</div>' : '';
const govBadge = gov ? '<span class="org-badge gov" title="Organo di amministrazione/direttivo (art. 23 D.Lgs. 138/2024)">Governance · Art. 23</span>' : '';
const children = (node.children && node.children.length)
? '<div class="org-children">' + node.children.map(orgNodeHtml).join('') + '</div>'
? '<div class="org-children">' + node.children.map(c => orgNodeHtml(c, seen)).join('') + '</div>'
: '';
return '' +
+1 -1
View File
@@ -157,7 +157,7 @@
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260731"></script>
<script src="/js/help.js?v=20260801"></script>
<script src="/js/organigramma.js?v=20260627"></script>
<script src="/js/organigramma.js?v=20260806"></script>
<script>
// Render icone Lucide (chrome + crumb + bottoni). organigramma.js fa render albero
// dopo il fetch dati: re-render icone su mutazioni del DOM dell'app.