(function () { 'use strict'; const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>'); const LEVEL_SHORT = { full: 'ADM', read: 'READ', link: 'LINK', api: 'API', system: 'SYS', none: '—', }; const state = { data: null, tab: 'overview', selectedRole: 'super_admin', softwareFilter: 'all', saving: false, saveError: null, }; async function patchApi(path, body) { const r = await fetchWithTimeout(`/api/v1${path}`, { method: 'PATCH', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(body), }); if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`); return r.json(); } function bindingMeta(agentId, roleId, relation) { const b = state.data?.agent_bindings_matrix?.agents?.[agentId]?.[roleId]?.[relation]; return { enabled: !!b?.enabled, locked: !!b?.locked, }; } function capMeta(capId, roleId) { const c = state.data?.agent_bindings_matrix?.caps?.[capId]?.[roleId]; return { enabled: !!c?.enabled, locked: !!c?.locked }; } async function toggleBinding(agentId, roleId, relation) { const meta = bindingMeta(agentId, roleId, relation); if (meta.locked) return; state.saving = true; state.saveError = null; try { await patchApi('/rbac/agent-bindings', { agent_id: agentId, role_id: roleId, relation, enabled: !meta.enabled, }); state.data = await api('/rbac/matrix'); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function toggleCap(capId, roleId) { const meta = capMeta(capId, roleId); if (meta.locked) return; state.saving = true; state.saveError = null; try { await patchApi('/rbac/agent-governance', { cap_id: capId, role_id: roleId, enabled: !meta.enabled, }); state.data = await api('/rbac/matrix'); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function api(path) { const r = await fetchWithTimeout(`/api/v1${path}`, { headers: authHeaders({ 'Content-Type': 'application/json' }), }); if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`); return r.json(); } function levelMeta(level) { return state.data.legend[level] || state.data.legend.none; } function badgeHtml(level, opts = {}) { const meta = levelMeta(level); const short = LEVEL_SHORT[meta.access] || '—'; const cls = opts.compact ? ' am-badge--compact' : ''; return `${short}`; } function serviceLabel(svc) { return state.data.service_labels?.[svc] || svc; } function rolesByCategory(catalog, categoryLabels) { const groups = {}; Object.values(catalog.roles || {}).forEach((role) => { const cat = role.category || 'platform'; if (!groups[cat]) groups[cat] = []; groups[cat].push(role); }); const order = ['ops', 'commercial', 'business', 'platform', 'system']; return order .filter((c) => groups[c]?.length) .map((c) => ({ id: c, label: categoryLabels[c] || c, roles: groups[c] })); } function roleDeskRows() { const roleId = state.selectedRole; return (state.data.desk_modules || []).map((row) => ({ ...row, level: row.levels[roleId] || 'none', })).filter((r) => r.level !== 'none'); } function renderRoleSidebar() { const groups = rolesByCategory(state.data.catalog, state.data.category_labels || {}); return groups.map((g) => `

${esc(g.label)}

${g.roles.map((r) => ` `).join('')}
`).join(''); } function renderScopeBar() { return `
${(state.data.scope_layers || []).map((l) => `
${esc(l.label)} ${esc(l.host)} ${esc(l.desc)}
`).join('')}
`; } function renderMatrixTable(rows, rowLabel, idKey) { const { role_columns, legend } = state.data; const cols = role_columns || []; const head = cols.map((c) => { const label = state.data.catalog.roles[c]?.label || c; const hl = c === state.selectedRole ? ' am-col-active' : ''; return `${esc(label.split(' ')[0])}`; }).join(''); const body = rows.map((row) => { const cells = cols.map((role) => { const lv = row.levels[role] || 'none'; const tdHl = role === state.selectedRole ? ' am-row-highlight' : ''; return `${badgeHtml(lv)}`; }).join(''); const sub = row.product ? `${esc(row.product)}` : `${esc(row[idKey] || '')}`; return ` ${esc(row.label)} ${sub} ${cells}`; }).join(''); return `
${head}${body}
${esc(rowLabel)}
`; } function renderDeskMatrix() { return `

Grelha completa — módulos internos do Desk (VM122). Use Visão da função para resumo ou Software & Infra para VM112/123.

${renderMatrixTable(state.data.desk_modules || [], 'Módulo Desk', 'id')}`; } function renderBindingCards(bindings, title) { if (!bindings.length) { return `

${title ? esc(title) + ' — ' : ''}sem registos para esta função.

`; } const byService = {}; bindings.forEach((b) => { if (!byService[b.service]) byService[b.service] = []; byService[b.service].push(b); }); return ` ${title ? `

${esc(title)}

` : ''}
${Object.entries(byService).flatMap(([svc, items]) => items.map((b) => `
${esc(serviceLabel(svc))} ${badgeHtml(b.access || 'full', { compact: true })}
${esc(b.type || 'binding')}
${esc(b.value)}
`) ).join('')}
`; } function renderSoftwareCards(rows, title) { if (!rows.length) { return `

${esc(title || 'Software')} — sem acesso directo.

`; } return ` ${title ? `

${esc(title)}

` : ''}
${rows.map((r) => { const meta = levelMeta(r.level); return `
${esc(r.label)} ${esc(r.product)}
${badgeHtml(r.level, { compact: true })}

${esc(r.group)}

${r.host && r.host !== '—' ? `${esc(r.host)}` : ''}
`; }).join('')}
`; } function renderDeskChips() { const rows = roleDeskRows(); if (!rows.length) { return '

Sem módulos Desk activos para esta função.

'; } return `
${rows.map((r) => ` ${esc(r.label)} ${badgeHtml(r.level, { compact: true })} `).join('')}
`; } function renderRoleStats(role) { const sw = state.data.role_software_summaries?.[state.selectedRole] || []; const bindings = role?.bindings || []; const desk = roleDeskRows(); return `
${desk.length}Módulos Desk
${sw.length}Software
${bindings.length}APIs / Grupos
${(state.data.role_agents_summaries?.[state.selectedRole] || []).length}Agentes
`; } function renderRoleOverview() { const role = state.data.catalog.roles[state.selectedRole]; const sw = state.data.role_software_summaries?.[state.selectedRole] || []; const bindings = role?.bindings || []; return ` ${renderScopeBar()} ${renderRoleStats(role)}

Desk VM122 — módulos activos

${renderDeskChips()}
${renderSoftwareCards(sw, 'Software & infra em escopo')}
${renderBindingCards(bindings, 'APIs, grupos Odoo e permissões')}

Agentics — capacidades da função

${renderRoleAgentCaps()} ${renderRoleAgentsSummary('')}
`; } function renderSoftwareMatrix() { const { software_groups } = state.data; const groups = (software_groups || []).filter((g) => state.softwareFilter === 'all' || g.id === state.softwareFilter ); const filters = [ { id: 'all', label: 'Todas as camadas' }, ...(software_groups || []).map((g) => ({ id: g.id, label: g.label.split('—')[0].trim() })), ]; const filterBar = `
${filters.map((f) => ` `).join('')}
`; const sections = groups.map((grp) => { const meta = [grp.host, grp.url].filter(Boolean).join(' · '); return `

${esc(grp.label)}

${meta ? `${esc(meta)}` : ''}
${renderMatrixTable(grp.items || [], 'Recurso', 'id')}
`; }).join(''); const sw = state.data.role_software_summaries?.[state.selectedRole] || []; return `

Matriz de software fora do Desk — VM112 (onboard/mail), VM123 (finance/hosting) e consolas de infra.

${renderSoftwareCards(sw, 'Resumo da função seleccionada')} ${filterBar}${sections}`; } function renderBindings() { const role = state.data.catalog.roles[state.selectedRole]; if (!role) return '

Função não encontrada

'; return `

Bindings estilo Odoo — grupos, roles e permissões provisionados por função.

${renderBindingCards(role.bindings || [], '')}`; } function roleLabel(roleId) { return state.data.catalog.roles[roleId]?.label || roleId; } function renderRelationChip(rel) { const labels = state.data.agent_relation_labels || { approve: 'Aprova', focus: 'Operador', ui: 'UI', }; return `${esc(labels[rel] || rel)}`; } function renderRoleChips(roleIds) { return (roleIds || []).map((r) => `${esc(roleLabel(r))}` ).join(''); } function renderAgentGovernanceLegend() { const labels = state.data.agent_relation_labels || {}; return `
${Object.entries(labels).map(([k, v]) => `${renderRelationChip(k)} ${esc(v)}` ).join('')}
`; } function renderRoleAgentCaps() { const capsDef = [ ['use_ui', 'UI Agentics'], ['trigger_runs', 'Disparar cenários'], ['approve_runbooks', 'Aprovar runbooks'], ['configure_models', 'Configurar LLM'], ]; const roleId = state.selectedRole; if (!state.data.editable) { const caps = state.data.role_agentic_caps?.[roleId] || []; if (!caps.length) { return '

Esta função não acede ao módulo Agentics.

'; } return `
${caps.map((c) => `${esc(c.label)}`).join('')}
`; } return `
${capsDef.map(([id, label]) => { const meta = capMeta(id, roleId); return ``; }).join('')}
`; } function renderRoleAgentsSummary(title) { const rows = state.data.role_agents_summaries?.[state.selectedRole] || []; if (!rows.length) { return `

${esc(title || 'Agentes')} — sem interacção directa.

`; } return ` ${title ? `

${esc(title)}

` : ''}
${rows.map((a) => `
${esc(a.id)} ${esc(a.name)}

${esc(a.role)}

${(a.relations || []).map(renderRelationChip).join('')}
`).join('')}
`; } function renderAgentCard(a) { const rels = a.role_relations?.[state.selectedRole] || []; const hit = rels.length > 0; return `
${esc(a.id)}
${esc(a.name)} ${esc(a.codename)}

${esc(a.role)}

${hit ? `
${rels.map(renderRelationChip).join('')} esta função
` : ''}
Aprova ${renderRoleChips(a.approvers)}
Operadores ${renderRoleChips(a.operators)}
${(a.reads || []).length ? `
${esc(a.reads.slice(0, 3).join(' · '))}
` : ''}
`; } function renderRelationToggle(agentId, roleId, relation) { const meta = bindingMeta(agentId, roleId, relation); const short = { ui: 'UI', focus: 'OP', approve: 'AP' }[relation] || relation; return ``; } function renderAgentMatrixCell(agentId, role, rels) { const tdHl = role === state.selectedRole ? ' am-row-highlight' : ''; if (!state.data.editable) { const inner = rels.length ? rels.map(renderRelationChip).join('') : ''; return `${inner}`; } return ` ${renderRelationToggle(agentId, role, 'ui')} ${renderRelationToggle(agentId, role, 'focus')} ${renderRelationToggle(agentId, role, 'approve')} `; } function renderAgentsMatrix() { const agents = state.data.agents || []; const cols = state.data.role_columns || []; const head = cols.map((c) => { const hl = c === state.selectedRole ? ' am-col-active' : ''; return `${esc(roleLabel(c).split(' ')[0])}`; }).join(''); const body = agents.map((a) => { const cells = cols.map((role) => { const rels = a.role_relations?.[role] || []; return renderAgentMatrixCell(a.id, role, rels); }).join(''); return ` ${esc(a.id)} · ${esc(a.name)} ${esc(a.role)} ${cells}`; }).join(''); return `

Matriz agente × função

${renderAgentGovernanceLegend()}
${head}${body}
Agente
`; } function renderAgents() { const caps = state.data.role_agentic_caps?.[state.selectedRole] || []; return `

Agentes A0–A7 usam conta agent_system. ${state.data.editable ? 'Clique UI / OP / AP para ligar ou desligar atribuições (audit activo).' : 'Três relações: UI · Operador · Aprova.'}

${state.saveError ? `

${esc(state.saveError)}

` : ''} ${(state.data.editable || caps.length) ? `

Capacidades — ${esc(roleLabel(state.selectedRole))}

${renderRoleAgentCaps()}
` : ''} ${renderAgentsMatrix()}

Detalhe por agente

${(state.data.agents || []).map(renderAgentCard).join('')}
`; } function renderLegend() { const legend = state.data.legend || {}; return Object.entries(legend).map(([key, l]) => `${badgeHtml(key, { compact: true })} ${esc(l.label)}` ).join(''); } function activeTabHint() { const tab = (state.data.tabs || []).find((t) => t.id === state.tab); return tab?.hint || ''; } function renderMainPanel() { const role = state.data.catalog.roles[state.selectedRole]; let body = ''; if (state.tab === 'overview') body = renderRoleOverview(); else if (state.tab === 'vm122') body = renderDeskMatrix(); else if (state.tab === 'software') body = renderSoftwareMatrix(); else if (state.tab === 'bindings') body = renderBindings(); else if (state.tab === 'agents') body = renderAgents(); return `
Spec 027 · RBAC

${esc(role?.label || state.selectedRole)}

${esc(role?.description || '')}

${activeTabHint() ? `

${esc(activeTabHint())}

` : ''}
${renderLegend()}
${body}
`; } function renderTabs() { return (state.data.tabs || []).map((t) => `` ).join(''); } function bindEvents(root) { root.querySelectorAll('[data-am-role]').forEach((btn) => { btn.addEventListener('click', () => { state.selectedRole = btn.dataset.amRole; paint(root); }); }); root.querySelectorAll('[data-am-tab]').forEach((btn) => { btn.addEventListener('click', () => { state.tab = btn.dataset.amTab; paint(root); }); }); root.querySelectorAll('[data-am-sw-filter]').forEach((btn) => { btn.addEventListener('click', () => { state.softwareFilter = btn.dataset.amSwFilter; paint(root); }); }); root.querySelectorAll('[data-am-toggle]').forEach((btn) => { btn.addEventListener('click', () => { if (state.saving || !state.data?.editable) return; const [agentId, roleId, relation] = btn.dataset.amToggle.split('|'); toggleBinding(agentId, roleId, relation); }); }); root.querySelectorAll('[data-am-cap]').forEach((btn) => { btn.addEventListener('click', () => { if (state.saving || !state.data?.editable) return; const [capId, roleId] = btn.dataset.amCap.split('|'); toggleCap(capId, roleId); }); }); } function paint(root) { root.innerHTML = `

Matriz de Acessos

Quatro camadas: Desk VM122 · VM112 · VM123 · Infra externa

${state.data?.editable ? 'Edição · audit ON' : 'Preview · read-only'}${state.saving ? ' · …' : ''}
${state.saveError ? `

${esc(state.saveError)}

` : ''}
${renderMainPanel()}
`; bindEvents(root); } async function renderAccessMatrix() { const root = document.getElementById('access-matrix-content'); if (!root) return; root.innerHTML = '

Carregando matriz de acessos…

'; try { state.data = await api('/rbac/matrix'); if (!state.selectedRole && state.data.role_columns?.length) { state.selectedRole = state.data.role_columns[0]; } paint(root); } catch (e) { root.innerHTML = `

Matriz indisponível: ${esc(e.message)}

`; } } window.renderAccessMatrix = renderAccessMatrix; window.DeskAccessMatrix = { renderAccessMatrix }; })();