(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) => `
${esc(r.label)}
`).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 `
${esc(rowLabel)} ${head}
${body}
`;
}
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.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) => `
${esc(f.label)} `).join('')}
`;
const sections = groups.map((grp) => {
const meta = [grp.host, grp.url].filter(Boolean).join(' · ');
return `
${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 `${esc(label)} `;
}).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.role)}
${hit ? `${rels.map(renderRelationChip).join('')} esta função
` : ''}
Aprova
${renderRoleChips(a.approvers)}
Operadores
${renderRoleChips(a.operators)}
${(a.reads || []).length ? `
Lê
${esc(a.reads.slice(0, 3).join(' · '))}
` : ''}
Regra Spec 027
${esc(a.approval_text)}
`;
}
function renderRelationToggle(agentId, roleId, relation) {
const meta = bindingMeta(agentId, roleId, relation);
const short = { ui: 'UI', focus: 'OP', approve: 'AP' }[relation] || relation;
return `${short} `;
}
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()}
`;
}
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 `
`;
}
function renderTabs() {
return (state.data.tabs || []).map((t) =>
`${esc(t.label)} `
).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 = `
${state.saveError ? `
${esc(state.saveError)}
` : ''}
${renderTabs()}
`;
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 };
})();