Governed action. Execution delayed by ${delayStr}. Cancellable during grace period.
RESOURCE
${resourceType} / ${name}
`;
document.body.appendChild(overlay);
const btn = overlay.querySelector('#idm-confirm-btn');
overlay.querySelector('#idm-confirm').addEventListener('input', e => {
const ok = e.target.value === typeTarget;
btn.disabled = !ok;
btn.style.opacity = ok ? '0.9' : '0.4';
});
btn.onclick = () => infraExecDelete(resourceType, name);
}
/* ── Action toast ───────────────────────────────────────────────────── */
async function infraExec(label, apiCall) {
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:var(--bg2);border:1px solid var(--border2);border-radius:var(--radius2);padding:12px 18px;font-size:12px;color:var(--text);z-index:9998;font-family:var(--mono);';
toast.textContent = `⟳ ${label}…`;
document.body.appendChild(toast);
try {
const result = await apiCall();
toast.style.borderColor = 'var(--green)'; toast.style.color = 'var(--green)';
toast.textContent = `✓ ${label}`;
setTimeout(() => toast.remove(), 3000);
return result;
} catch(e) {
toast.style.borderColor = 'var(--red)'; toast.style.color = 'var(--red)';
toast.textContent = `✗ ${label}: ${e.message}`;
setTimeout(() => toast.remove(), 5000);
throw e;
}
}
async function infraExecDelete(resourceType, name) {
const reason = document.getElementById('idm-reason')?.value?.trim();
const confirmInput = document.getElementById('idm-confirm')?.value?.trim();
if (!reason) { document.getElementById('idm-reason').style.borderColor='var(--red)'; return; }
document.getElementById('infra-danger-overlay')?.remove();
try {
const result = await infraExec(`Request deletion: ${name}`, () =>
Shell.api('/api/governance/delete-request', { method:'POST',
body: JSON.stringify({ resource_type:resourceType, name, reason, confirm_input:confirmInput }) })
);
const notice = document.createElement('div');
notice.style.cssText = 'position:fixed;top:70px;right:24px;background:var(--bg2);border:1px solid var(--amber);border-radius:var(--radius2);padding:14px 18px;font-size:12px;z-index:9998;max-width:360px;';
const mins = Math.round((result.execute_in_seconds||3600)/60);
notice.innerHTML = `Cancel in Delete Queue.
`;
document.body.appendChild(notice);
setTimeout(() => notice.remove(), 8000);
Modules.infrastructure(document.getElementById('content'));
} catch(e) {
const notice = document.createElement('div');
notice.style.cssText = 'position:fixed;top:70px;right:24px;background:var(--bg2);border:1px solid var(--red);border-radius:var(--radius2);padding:14px 18px;font-size:12px;z-index:9998;max-width:360px;';
notice.innerHTML = ` { pendingMap[e.name] = e; });
const getState = (type, name) => pendingMap[name] ? 'pending_delete' : (govStates[type]?.[name] || 'active');
const expiredC = (certs.certs||[]).filter(c => certMap[c.domain]?.status==='expired');
const expiringC = (certs.certs||[]).filter(c => certMap[c.domain]?.status==='expiring');
const cleanMail = (mailboxes.mailboxes||[]).filter(m =>
!['nobody','systemd-network','systemd-timesync','systemd-resolve','fwupd-refresh','polkitd'].includes(m));
/* ── action buttons helper ── */
const actionBtns = (type, name) => {
const st = getState(type, name);
const rem = pendingMap[name] ? Math.max(0, pendingMap[name].execute_at - now/1000) : 0;
if (st==='pending_delete') return `
⏱ ${Math.round(rem/60)}m
`;
if (st==='suspended') return `
SUSPENDED
`;
return `
`;
};
const stateDot = (type, name) => {
const st = getState(type, name);
return st==='pending_delete'?'var(--red)':st==='suspended'?'var(--amber)':'';
};
body.innerHTML = `
${[
['DNS Zones', zones.zones?.length||0, 'var(--accent)'],
['Mailboxes', cleanMail.length, 'var(--green)'],
['Databases', databases.databases?.length||0, 'var(--purple)'],
['Web Sites', sites.sites?.length||0, 'var(--amber)'],
['SSL Certs', certs.certs?.length||0, expiredC.length?'var(--red)':expiringC.length?'var(--amber)':'var(--green)'],
].map(([l,v,c]) => `
`).join('')}
${(expiredC.length||expiringC.length)?`
⚠ Audit-Driven Recommendations
${[...expiredC,...expiringC].map(c=>`
●
${c.domain}
${certMap[c.domain].status==='expired'?'SSL EXPIRED':'expiring '+c.expires}
`).join('')}
`:''}
◈ DNS Zones
${zones.zones?.length||0} · threeEdnsA→B
${(zones.zones||[]).map(z=>{
const dot = stateDot('dns_zone',z)||'var(--green)';
const st = getState('dns_zone',z);
return `
●
${z}
${st==='active'?`
`:''}
${actionBtns('dns_zone',z)}
`;
}).join('')}
◈ Email Mailboxes
${cleanMail.length} · mail.3enet.ca
${cleanMail.map(m=>{
const dot = stateDot('email',m)||'var(--accent)';
const st = getState('email',m);
return `
●
${m}@3enet.ca
${actionBtns('email',m)}
`;
}).join('')}
◈ Databases
${databases.databases?.length||0} · PostgreSQL
${(databases.databases||[]).map(d=>{
const dot = stateDot('database',d)||'var(--purple)';
const st = getState('database',d);
return `
●
${d}
PostgreSQL
${actionBtns('database',d)}
`;
}).join('')}
◈ Web Sites
${sites.sites?.length||0} · nginx
${(sites.sites||[]).map(s=>{
const cert=certMap[s];
const dot=!cert?'var(--text3)':cert.status==='expired'?'var(--red)':cert.status==='expiring'?'var(--amber)':'var(--green)';
const ssl=!cert?'no SSL':cert.status==='expired'?'SSL EXPIRED':cert.status==='expiring'?'expiring':'SSL ✓';
return `
●
${s}
${ssl}
${actionBtns('webspace',s)}
`;
}).join('')}
${(certs.certs||[]).map(c=>{
const st=certMap[c.domain]?.status||'ok';
const dot=st==='expired'?'var(--red)':st==='expiring'?'var(--amber)':'var(--green)';
return `
●
${c.domain}
${c.expires}
${st.toUpperCase()}
`;
}).join('')}
PHASE 3 · ENTERPRISE GOVERNANCE · All destructive actions are time-locked and audited
`;
} catch(e) {
body.innerHTML = `Failed: ${e.message}
`;
}
};