/home/techb158/cosmic.abdallabala.com/public
Edit: /home/techb158/cosmic.abdallabala.com/public/app.js (68455B)
(function () {
"use strict";
const DEFAULT_PROJECT_ID = "cosmic-ai-risk-001";
const LIFECYCLE_PHASES = [
"Needs identification",
"Data collection and preparation",
"Design",
"Model development and training",
"Testing and performance evaluation",
"Deployment, support, and monitoring"
];
const state = {
rawData: null,
dashboard: null,
apiAvailable: false,
projectId: DEFAULT_PROJECT_ID,
filters: {
dimension: "All",
status: "All",
severity: "All",
search: ""
},
mitigationFilters: {
status: "All",
search: ""
},
gateHistory: [],
auditEvents: [],
selectedGateId: null,
pmIntegrations: [],
pmMappings: [],
pmSyncRuns: [],
actorUserId: localStorage.getItem("cosmicActorUserId") || "USER-SYSTEM",
access: null
};
const $ = selector => document.querySelector(selector);
const $$ = selector => Array.from(document.querySelectorAll(selector));
document.addEventListener("DOMContentLoaded", init);
async function init() {
bindTabs();
bindActions();
bindCrudForms();
await loadData();
}
async function loadData() {
if (window.EMBEDDED_COSMIC_DATA) {
state.rawData = window.EMBEDDED_COSMIC_DATA;
state.dashboard = window.CosmicRiskEngine.calculateDashboard(state.rawData);
state.apiAvailable = false;
state.projectId = getProjectId();
renderDashboard();
return;
}
try {
const apiData = await apiJson("/api/dashboard");
state.rawData = apiData.data;
state.dashboard = apiData.dashboard;
state.apiAvailable = true;
state.projectId = getProjectId();
await loadAccessControl();
await loadGateWorkflow();
await loadIntegrationWorkflow();
} catch (error) {
const response = await fetch("data/sample-project.json").catch(() => null);
if (!response || !response.ok) {
showError("Dashboard data could not be loaded. Use node server.js or place this folder on a local web server.");
return;
}
state.rawData = await response.json();
state.dashboard = window.CosmicRiskEngine.calculateDashboard(state.rawData);
state.apiAvailable = false;
state.projectId = getProjectId();
state.access = null;
}
renderDashboard();
}
async function apiJson(path, options) {
const merged = Object.assign({ headers: {} }, options || {});
merged.headers = Object.assign({
"Content-Type": "application/json",
"X-Cosmic-User-Id": state.actorUserId || "USER-SYSTEM"
}, merged.headers || {});
const response = await fetch(path, merged);
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`);
return data;
}
async function loadAccessControl() {
if (!state.apiAvailable) {
state.access = null;
return;
}
try {
state.access = await apiJson("/api/access/me");
} catch (error) {
state.access = null;
console.warn(error);
}
}
async function loadGateWorkflow() {
if (!state.apiAvailable) {
state.gateHistory = [];
state.auditEvents = [];
state.selectedGateId = null;
return;
}
try {
const [history, audit] = await Promise.all([
apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/gate/history`),
apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/audit`)
]);
state.gateHistory = history.gates || [];
state.auditEvents = audit.auditEvents || [];
if (!state.selectedGateId && state.gateHistory.length) {
state.selectedGateId = state.gateHistory[0].id;
}
if (state.selectedGateId && !state.gateHistory.some(gate => gate.id === state.selectedGateId)) {
state.selectedGateId = state.gateHistory[0] ? state.gateHistory[0].id : null;
}
} catch (error) {
state.gateHistory = [];
state.auditEvents = [];
console.warn(error);
}
}
async function loadIntegrationWorkflow() {
if (!state.apiAvailable) {
state.pmIntegrations = [];
state.pmMappings = [];
state.pmSyncRuns = [];
return;
}
try {
const [integrations, mappings, syncRuns] = await Promise.all([
apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/integrations`),
apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/integration-mappings`),
apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/sync-runs`)
]);
state.pmIntegrations = integrations.integrations || [];
state.pmMappings = mappings.mappings || [];
state.pmSyncRuns = syncRuns.syncRuns || [];
} catch (error) {
state.pmIntegrations = [];
state.pmMappings = [];
state.pmSyncRuns = [];
console.warn(error);
}
}
function getProjectId() {
return (state.rawData && state.rawData.project && state.rawData.project.id) || DEFAULT_PROJECT_ID;
}
function bindTabs() {
$$(".nav-button").forEach(button => {
button.addEventListener("click", () => {
$$(".nav-button").forEach(item => item.classList.remove("active"));
$$(".view").forEach(view => view.classList.remove("active"));
button.classList.add("active");
const target = button.getAttribute("data-view");
$(target).classList.add("active");
});
});
}
function bindActions() {
$("#dimensionFilter").addEventListener("change", event => {
state.filters.dimension = event.target.value;
renderRiskTable();
});
$("#statusFilter").addEventListener("change", event => {
state.filters.status = event.target.value;
renderRiskTable();
});
$("#severityFilter").addEventListener("change", event => {
state.filters.severity = event.target.value;
renderRiskTable();
});
$("#riskSearch").addEventListener("input", event => {
state.filters.search = event.target.value.toLowerCase();
renderRiskTable();
});
$("#mitigationStatusFilter").addEventListener("change", event => {
state.mitigationFilters.status = event.target.value;
renderMitigations(state.dashboard);
});
$("#mitigationSearch").addEventListener("input", event => {
state.mitigationFilters.search = event.target.value.toLowerCase();
renderMitigations(state.dashboard);
});
$("#refreshDashboard").addEventListener("click", () => loadData().then(() => showToast("Dashboard refreshed.")));
$("#exportJson").addEventListener("click", () => {
if (state.apiAvailable) {
downloadReportFromUi("full.json", "cosmic-ai-risk-full-report.json");
return;
}
const payload = {
exportedAt: new Date().toISOString(),
projectId: state.projectId,
dashboard: state.dashboard
};
downloadFile("cosmic-ai-risk-dashboard-report.json", JSON.stringify(payload, null, 2), "application/json");
});
$("#printReport").addEventListener("click", () => {
if (state.apiAvailable) openReportFromUi("executive.html");
else window.print();
});
bindReportActions();
$("#createRiskButton").addEventListener("click", openCreateRiskDialog);
$("#createMitigationButton").addEventListener("click", () => openCreateMitigationDialog());
$("#evaluateGateButton").addEventListener("click", evaluateGateFromUi);
$("#saveGateDecisionButton").addEventListener("click", saveGateDecisionFromUi);
$("#saveGateCriteriaButton").addEventListener("click", saveGateCriteriaNotesFromUi);
const actorSelect = $("#actorUserSelect");
if (actorSelect) {
actorSelect.addEventListener("change", async event => {
state.actorUserId = event.target.value || "USER-SYSTEM";
localStorage.setItem("cosmicActorUserId", state.actorUserId);
await loadData();
showToast(`Current actor changed to ${state.actorUserId}.`);
});
}
const reloadAccess = $("#reloadAccessButton");
if (reloadAccess) reloadAccess.addEventListener("click", () => loadData().then(() => showToast("Access control reloaded.")));
$("#gateHistoryTable").addEventListener("click", event => {
const button = event.target.closest("button[data-action='select-gate']");
if (!button) return;
state.selectedGateId = button.getAttribute("data-gate-id");
renderGate(state.dashboard);
});
$("#pmIntegrationTable").addEventListener("click", event => {
const button = event.target.closest("button[data-action]");
if (!button) return;
const integrationId = button.getAttribute("data-integration-id");
const action = button.getAttribute("data-action");
if (action === "sync-integration") syncIntegrationFromUi(integrationId);
if (action === "oauth-integration") authorizeIntegrationFromUi(integrationId);
if (action === "live-test-integration") testLiveIntegrationFromUi(integrationId);
if (action === "live-sync-integration") syncLiveIntegrationFromUi(integrationId);
});
$("#riskTable").addEventListener("click", event => {
const button = event.target.closest("button[data-action]");
if (!button) return;
const riskId = button.getAttribute("data-risk-id");
const action = button.getAttribute("data-action");
const risk = findRisk(riskId);
if (!risk) return showToast("Risk could not be found.", "error");
if (action === "edit") openEditRiskDialog(risk);
if (action === "mitigate") openCreateMitigationDialog(risk);
if (action === "delete") deleteRiskFromUi(risk);
});
$("#mitigationTable").addEventListener("click", event => {
const button = event.target.closest("button[data-action]");
if (!button) return;
const mitigationId = button.getAttribute("data-mitigation-id");
const action = button.getAttribute("data-action");
const mitigation = findMitigation(mitigationId);
if (!mitigation) return showToast("Mitigation action could not be found.", "error");
if (action === "edit-mitigation") openEditMitigationDialog(mitigation);
if (action === "done-mitigation") markMitigationDoneFromUi(mitigation);
if (action === "delete-mitigation") deleteMitigationFromUi(mitigation);
});
}
function bindReportActions() {
const bindings = [
["#openExecutiveReport", () => openReportFromUi("executive.html")],
["#downloadFullJsonReport", () => downloadReportFromUi("full.json", "cosmic-ai-risk-full-report.json")],
["#downloadRiskCsvReport", () => downloadReportFromUi("risk-register.csv", "cosmic-ai-risk-register.csv")],
["#downloadMitigationCsvReport", () => downloadReportFromUi("mitigations.csv", "cosmic-ai-risk-mitigations.csv")],
["#downloadGateJsonReport", () => downloadJsonReportFromUi("gate", "cosmic-ai-risk-gate-report.json")],
["#downloadAuditJsonReport", () => downloadJsonReportFromUi("audit", "cosmic-ai-risk-audit-report.json")]
];
bindings.forEach(([selector, handler]) => {
const button = $(selector);
if (button) button.addEventListener("click", handler);
});
}
function bindCrudForms() {
$$("[data-close-dialog]").forEach(button => {
button.addEventListener("click", () => {
const dialog = document.getElementById(button.getAttribute("data-close-dialog"));
if (dialog) dialog.close();
});
});
["riskProbability", "riskImpact", "riskDetectability"].forEach(id => {
const element = document.getElementById(id);
if (element) element.addEventListener("input", updateRiskScorePreview);
});
$("#riskForm").addEventListener("submit", event => {
event.preventDefault();
saveRiskFromUi();
});
$("#mitigationForm").addEventListener("submit", event => {
event.preventDefault();
saveMitigationFromUi();
});
}
function renderDashboard() {
const data = state.dashboard;
if (!data) return;
renderHeader(data);
renderSummaryCards(data);
renderDimensionBars(data);
renderTriangle(data);
renderLifecycle(data);
renderTopRisks(data);
renderRiskTable();
renderMitigations(data);
renderGate(data);
renderIndicators(data);
renderExperiments(data);
renderPmIntegrations();
renderReports(data);
renderAccessControl();
renderSourceAlignment();
renderApiState();
}
function renderHeader(data) {
$("#projectName").textContent = data.project.name;
$("#projectSubtitle").textContent = data.project.subtitle;
$("#projectMeta").textContent = `${data.project.projectType} · ${data.project.lifecyclePhase} · Assessment ${data.summary.assessmentDate}`;
}
function renderApiState() {
const buttonRules = [
["#createRiskButton", "risk:write"],
["#createMitigationButton", "mitigation:write"],
["#evaluateGateButton", "gate:evaluate"],
["#saveGateDecisionButton", "gate:review"],
["#saveGateCriteriaButton", "gate:review"],
["#openExecutiveReport", "report:export"],
["#downloadFullJsonReport", "report:export"],
["#downloadRiskCsvReport", "report:export"],
["#downloadMitigationCsvReport", "report:export"],
["#downloadGateJsonReport", "report:export"],
["#downloadAuditJsonReport", "report:export"]
];
buttonRules.forEach(([selector, permission]) => {
const button = $(selector);
if (!button) return;
const allowed = state.apiAvailable && hasPermission(permission);
button.disabled = !allowed;
button.title = allowed ? "" : permissionHelp(permission);
});
const actor = state.access && state.access.actor;
const badge = $("#currentActorBadge");
if (badge) badge.textContent = actor ? `Actor: ${actor.displayName} · ${actor.roleName}` : "Actor: offline demo";
}
function renderSummaryCards(data) {
const cards = [
{
label: "Overall AI risk score",
value: `${data.summary.overallScore}/100`,
detail: `${data.summary.riskLevel} residual exposure`,
status: data.summary.riskLevel
},
{
label: "Deployment gate",
value: data.summary.gateStatus,
detail: data.gate.message,
status: data.summary.gateStatus
},
{
label: "Open risks",
value: data.summary.openRisks,
detail: `${data.summary.totalRisks} total risks tracked`,
status: "Neutral"
},
{
label: "Mitigation completion",
value: `${data.summary.mitigationCompletion}%`,
detail: "Average progress across active risks",
status: data.summary.mitigationCompletion >= 60 ? "Pass" : "Blocked"
},
{
label: "Critical risks",
value: data.summary.criticalRisks,
detail: `${data.summary.highRisks} high risks also active`,
status: data.summary.criticalRisks > 0 ? "Critical" : "Pass"
}
];
$("#summaryCards").innerHTML = cards.map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
}
function renderDimensionBars(data) {
const rows = Object.entries(data.dimensions).map(([dimension, value]) => `
${escapeHtml(dimension)}
${value}/100
`).join("");
$("#dimensionBars").innerHTML = rows;
}
function renderTriangle(data) {
const org = data.dimensions.Organizational || 0;
const tech = data.dimensions.Technical || 0;
const human = data.dimensions.Human || 0;
$("#trianglePanel").innerHTML = `
${org}
${tech}
${human}
Organizational
Technical
Human
AI Risks
Governance · Metrology · Evidence
`;
}
function renderLifecycle(data) {
$("#lifecycleGrid").innerHTML = data.lifecycle.map(phase => `
${escapeHtml(phase.id)}
${escapeHtml(phase.name)}
Readiness ${phase.readiness}/100
${phase.openRisks} open risks
`).join("");
}
function renderTopRisks(data) {
$("#topRisks").innerHTML = data.topRisks.map(risk => `
${escapeHtml(risk.id)}
${escapeHtml(risk.title)}
${escapeHtml(risk.dimension)}
${risk.residualScore}/100
${escapeHtml(risk.residualSeverity)}
${escapeHtml(risk.owner)}
`).join("");
}
function renderRiskTable() {
const rows = filteredRisks(state.dashboard.scoredRisks);
$("#riskCount").textContent = `${rows.length} risks shown`;
$("#riskTable").innerHTML = rows.map(risk => `
${escapeHtml(risk.id)}
${escapeHtml(risk.title)}
${escapeHtml(risk.evidence || risk.description || "No evidence summary recorded")}
${escapeHtml(risk.dimension)}
${escapeHtml(risk.lifecyclePhase)}
${risk.probability}
${risk.impact}
${risk.detectability}
${risk.normalizedScore}
${risk.residualScore}
${escapeHtml(risk.status)}
${escapeHtml(risk.approvalStatus)}
${escapeHtml(risk.owner)}
${renderMitigationSummary(risk)}
${renderRiskActions(risk)}
`).join("");
}
function renderMitigations(data) {
if (!data) return;
const mitigations = filteredMitigations(data.mitigations || []);
renderMitigationSummaryCards(data.mitigations || []);
$("#mitigationCount").textContent = `${mitigations.length} mitigation actions shown`;
$("#mitigationTable").innerHTML = mitigations.map(item => `
${escapeHtml(item.id)}
${escapeHtml(item.title)}
${escapeHtml(item.description || "No mitigation description recorded")}
${escapeHtml(item.riskId)}
${escapeHtml(item.riskTitle)}
${escapeHtml(item.riskDimension)}
${escapeHtml(item.status)}
${renderPercentBar(item.progress_percent, "Progress")}
${renderPercentBar(item.effectiveness_percent, "Effectiveness")}
${renderDueDate(item.dueDate, item.status)}
${escapeHtml(item.owner)}
${renderEvidenceLinks(item.evidenceArtifacts || [], item.id)}
${item.riskResidualScore == null ? "Missing" : `${item.riskResidualScore}/100`}
${escapeHtml(item.riskResidualSeverity || "Unknown")} residual risk
${renderMitigationActions(item)}
`).join("");
}
function renderMitigationSummaryCards(mitigations) {
const total = mitigations.length;
const done = mitigations.filter(item => item.status === "Done").length;
const inProgress = mitigations.filter(item => item.status === "In progress").length;
const avgProgress = averageNumbers(mitigations.map(item => item.progress_percent));
const avgEffectiveness = averageNumbers(mitigations.map(item => item.effectiveness_percent));
const overdue = mitigations.filter(item => isOverdue(item.dueDate, item.status)).length;
const cards = [
{ label: "Mitigation actions", value: total, detail: `${done} done, ${inProgress} in progress`, status: "Neutral" },
{ label: "Average progress", value: `${avgProgress}%`, detail: "Used by residual-risk calculation", status: avgProgress >= 60 ? "Pass" : "Warning" },
{ label: "Average effectiveness", value: `${avgEffectiveness}%`, detail: "Estimated control effectiveness", status: avgEffectiveness >= 60 ? "Pass" : "Warning" },
{ label: "Overdue mitigations", value: overdue, detail: "Open actions past due date", status: overdue > 0 ? "Blocked" : "Pass" }
];
$("#mitigationSummaryCards").innerHTML = cards.map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
}
function filteredMitigations(mitigations) {
return mitigations.filter(item => {
const matchesStatus = state.mitigationFilters.status === "All" || item.status === state.mitigationFilters.status;
const search = state.mitigationFilters.search;
const haystack = [item.id, item.title, item.description, item.riskId, item.riskTitle, item.owner, item.riskOwner]
.concat((item.evidenceArtifacts || []).map(evidence => `${evidence.title} ${evidence.uri || ""}`))
.join(" ").toLowerCase();
return matchesStatus && (!search || haystack.includes(search));
});
}
function renderPercentBar(value, label) {
const safeValue = Math.max(0, Math.min(100, Number(value) || 0));
return `
${safeValue}%
${escapeHtml(label)}
`;
}
function renderDueDate(dateText, status) {
if (!dateText) return '
No due date ';
const overdue = isOverdue(dateText, status);
return `
${escapeHtml(dateText)} ${overdue ? "
Overdue " : ""}`;
}
function renderEvidenceLinks(evidenceArtifacts, mitigationId) {
const linked = evidenceArtifacts.filter(item => !item.mitigation_id || item.mitigation_id === mitigationId || item.mitigationId === mitigationId);
const items = linked.slice(0, 3);
if (!items.length) return '
No evidence linked ';
return items.map(item => {
const label = escapeHtml(item.title || item.uri || item.id);
if (item.uri) return `
${label} `;
return `
${label} `;
}).join("");
}
function renderMitigationActions(item) {
const disabled = state.apiAvailable ? "" : ' disabled title="Start the Node.js server to enable write actions."';
return `
Edit
Mark done
Delete
`;
}
function renderMitigationSummary(risk) {
const actions = risk.mitigationActions || [];
if (!actions.length) return "
None ";
const done = actions.filter(item => item.status === "Done").length;
return `
${actions.length} action${actions.length === 1 ? "" : "s"}
${done} done · ${risk.mitigationProgress || 0}% progress · ${risk.mitigationEffectiveness || 0}% effective
`;
}
function renderRiskActions(risk) {
const disabled = state.apiAvailable ? "" : " disabled title=\"Start the Node.js server to enable write actions.\"";
return `
Edit
Mitigate
Delete
`;
}
function filteredRisks(risks) {
return risks.filter(risk => {
const matchesDimension = state.filters.dimension === "All" || risk.dimension === state.filters.dimension;
const matchesStatus = state.filters.status === "All" || risk.status === state.filters.status;
const matchesSeverity = state.filters.severity === "All" || risk.residualSeverity === state.filters.severity;
const search = state.filters.search;
const matchesSearch = !search || [risk.id, risk.title, risk.owner, risk.lifecyclePhase, risk.domain, risk.evidence].join(" ").toLowerCase().includes(search);
return matchesDimension && matchesStatus && matchesSeverity && matchesSearch;
});
}
function renderGate(data) {
const selectedGate = getSelectedGate();
const currentGate = data.gate;
const displayGate = selectedGate || {
status: currentGate.status,
reviewStatus: "Not persisted",
summary: currentGate.message,
evaluatedAt: data.summary.assessmentDate,
criteria: currentGate.criteria.map(item => ({
id: item.id,
name: item.name,
expectedRule: item.required,
actualValue: item.actual,
status: item.status,
evidence: item.evidence,
reviewerStatus: "Open",
reviewerNote: "",
evidenceUri: "",
evidenceTitle: item.evidence
})),
decisions: [],
latestDecision: null,
blockingCriteria: currentGate.blockedCount,
reviewerNotes: 0,
evidenceLinks: 0
};
$("#gateStatusBadge").textContent = currentGate.status;
$("#gateStatusBadge").className = `status-badge ${statusClass(currentGate.status)}`;
$("#gateSummaryText").textContent = currentGate.message;
renderGateWorkflowSummary(currentGate, displayGate);
renderGateCriteria(displayGate);
renderGateHistory();
renderGateDecisionForm(displayGate);
renderAuditTrail();
}
function getSelectedGate() {
if (!state.gateHistory.length) return null;
return state.gateHistory.find(gate => gate.id === state.selectedGateId) || state.gateHistory[0];
}
function renderGateWorkflowSummary(currentGate, selectedGate) {
const latestDecision = selectedGate && selectedGate.latestDecision;
const cards = [
{ label: "Current calculated gate", value: currentGate.status, detail: currentGate.message, status: currentGate.status },
{ label: "Persisted evaluations", value: state.gateHistory.length, detail: selectedGate && selectedGate.id ? `Selected ${selectedGate.id}` : "No persisted gate yet", status: "Neutral" },
{ label: "Reviewer decision", value: latestDecision ? latestDecision.decision : "Pending", detail: latestDecision ? latestDecision.reason || "Decision recorded" : "Run a gate evaluation, then record a reviewer decision", status: latestDecision ? latestDecision.decision : "Warning" },
{ label: "Evidence links", value: selectedGate ? selectedGate.evidenceLinks || 0 : 0, detail: `${selectedGate ? selectedGate.reviewerNotes || 0 : 0} reviewer notes recorded`, status: selectedGate && selectedGate.evidenceLinks ? "Pass" : "Warning" }
];
$("#gateWorkflowCards").innerHTML = cards.map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
}
function renderGateCriteria(gate) {
$("#gateCriteria").innerHTML = (gate.criteria || []).map(item => `
${escapeHtml(item.id)}
${escapeHtml(item.name)}
${escapeHtml(item.expectedRule || item.required)}
${escapeHtml(item.actualValue == null ? item.actual : item.actualValue)}
${escapeHtml(item.status)}
${escapeHtml(item.evidence || item.evidenceTitle || "No evidence recorded")}
${item.evidenceUri ? `Open linked evidence ` : ""}
${item.gateId ? `
${["Open", "Reviewed", "Evidence requested", "Accepted", "Rejected"].map(status => `${status} `).join("")}
${escapeHtml(item.reviewerNote || "")}
` : `Persist the gate to add notes. `}
`).join("");
}
function renderGateHistory() {
const rows = state.gateHistory || [];
$("#gateHistoryCount").textContent = `${rows.length} persisted gate evaluations`;
$("#gateHistoryTable").innerHTML = rows.map(gate => `
${escapeHtml(gate.id)}
${formatDateTime(gate.evaluatedAt)}
${escapeHtml(gate.status)}
${escapeHtml(gate.reviewStatus)}
${escapeHtml(gate.latestDecision ? gate.latestDecision.reviewerName : gate.reviewedBy || "Pending")}
${gate.blockingCriteria}
${gate.evidenceLinks}
Review
`).join("") || `
No persisted gate evaluation yet. Use Evaluate and persist gate. `;
}
function renderGateDecisionForm(gate) {
const hasPersistedGate = Boolean(gate && gate.id);
$("#selectedGateForDecision").textContent = hasPersistedGate
? `Selected gate ${gate.id}, evaluated ${formatDateTime(gate.evaluatedAt)}`
: "No persisted gate selected. Evaluate and persist the gate first.";
$("#saveGateDecisionButton").disabled = !state.apiAvailable || !hasPersistedGate;
$("#saveGateCriteriaButton").disabled = !state.apiAvailable || !hasPersistedGate;
}
function renderAuditTrail() {
const rows = state.auditEvents || [];
$("#auditTrailTable").innerHTML = rows.slice(0, 30).map(event => `
${escapeHtml(event.id)}
${formatDateTime(event.createdAt)}
${escapeHtml(event.actorUserId)}
${escapeHtml(event.entityType)}
${escapeHtml(event.entityId)}
${escapeHtml(event.action)}
`).join("") || `
No audit events recorded yet. `;
}
function renderIndicators(data) {
$("#indicatorCards").innerHTML = data.indicators.map(indicator => `
${escapeHtml(indicator.id)}
${escapeHtml(indicator.name)}
Measurand: ${escapeHtml(indicator.measurand)}
Unit: ${escapeHtml(indicator.unit)}
${escapeHtml(indicator.interpretation)}
`).join("");
}
function renderExperiments(data) {
$("#experimentTable").innerHTML = data.experiments.map(exp => `
${escapeHtml(exp.id)}
${escapeHtml(exp.name)}${exp.selected ? " Selected " : ""}
${escapeHtml(exp.model)}
${formatMetric(exp.accuracy)}
${formatMetric(exp.f1)}
${formatMetric(exp.precision)}
${formatMetric(exp.recall)}
${formatMetric(exp.auc)}
${escapeHtml(exp.biasStatus)}
${escapeHtml(exp.stability)}
${escapeHtml(exp.reproducibility)}
`).join("");
}
function renderPmIntegrations() {
const integrations = state.pmIntegrations || [];
const mappings = state.pmMappings || [];
const syncRuns = state.pmSyncRuns || [];
const connected = integrations.filter(item => item.connectionStatus === "Connected").length;
const providers = integrations.map(item => item.provider).join(", ") || "Trello, Jira, Asana";
$("#pmIntegrationSummaryCards").innerHTML = [
{ label: "Supported PM apps", value: integrations.length || 4, detail: providers, status: "Neutral" },
{ label: "Connected apps", value: connected, detail: `${integrations.length - connected} need configuration or sync`, status: connected >= 4 ? "Pass" : "Warning" },
{ label: "External mappings", value: mappings.length, detail: "COSMIC risks linked to external work items", status: mappings.length ? "Pass" : "Warning" },
{ label: "Sync runs", value: syncRuns.length, detail: "Recorded synchronization attempts", status: syncRuns.length ? "Pass" : "Neutral" }
].map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
$("#pmIntegrationTable").innerHTML = integrations.map(item => {
const capabilities = item.capabilities || {};
return `
${escapeHtml(item.provider)} ${escapeHtml(item.authMode)}
${escapeHtml(item.workspaceName)}${escapeHtml(item.externalProjectKey)}
${escapeHtml(item.connectionStatus)}
${escapeHtml(item.syncDirection)}
${escapeHtml(capabilities.workItemName || "Work item")}
Risk score: ${escapeHtml(capabilities.riskFieldName || "Field")}
Mitigation: ${escapeHtml(capabilities.mitigationObjectName || "Linked item")}
${formatDateTime(item.lastSyncAt)}
OAuth URL
Live test
Live sync
Simulate
`;
}).join("");
$("#pmMappingTable").innerHTML = mappings.length ? mappings.map(item => `
${escapeHtml(item.provider)}
${escapeHtml(item.localEntityId)} ${escapeHtml(item.localTitle)}
${escapeHtml(item.externalItemType)} ${escapeHtml(item.externalItemKey)} ${item.externalUrl ? `Open external item ` : ""}
${escapeHtml(item.externalStatus)}
${escapeHtml(item.syncStatus)}
${formatDateTime(item.lastSyncedAt)}
`).join("") : `
No external mappings yet. Use Sync risks for Trello, Jira, Asana, or Microsoft Planner. `;
$("#pmSyncRunTable").innerHTML = syncRuns.length ? syncRuns.map(run => `
${escapeHtml(run.id)} ${formatDateTime(run.startedAt)}
${escapeHtml(run.provider)}
${escapeHtml(run.status)}
${run.createdCount}
${run.updatedCount}
${run.failedCount}
${escapeHtml(run.summary)}
`).join("") : `
No sync runs recorded yet. `;
}
function renderReports(data) {
if (!data) return;
const mitigations = data.mitigations || [];
const mappedApps = (state.pmIntegrations || []).length;
const connectedApps = (state.pmIntegrations || []).filter(item => item.connectionStatus === "Connected").length;
const auditCount = (state.auditEvents || []).length;
const cards = [
{ label: "Report package", value: "7 exports", detail: "Executive, full JSON, risks, mitigations, gate, indicators, audit", status: "Neutral" },
{ label: "Risk records", value: data.summary.totalRisks, detail: `${data.summary.openRisks} open risks`, status: data.summary.criticalRisks ? "Blocked" : "Pass" },
{ label: "Mitigation records", value: mitigations.length, detail: `${averageNumbers(mitigations.map(item => item.progress_percent))}% average progress`, status: data.summary.mitigationCompletion >= 60 ? "Pass" : "Warning" },
{ label: "PM applications", value: mappedApps || 4, detail: `${connectedApps} connected, ${(state.pmMappings || []).length} mappings`, status: connectedApps >= 4 ? "Pass" : "Warning" },
{ label: "Audit events", value: auditCount, detail: "Reviewer and workflow traceability", status: auditCount ? "Pass" : "Warning" }
];
const summary = $("#reportSummaryCards");
if (summary) {
summary.innerHTML = cards.map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
}
const coverage = [
"Executive summary with overall risk score and deployment gate result",
"Governance triangle scores for organizational, technical, and human dimensions",
"Lifecycle readiness map from needs identification to deployment monitoring",
"Risk register with raw, normalized, and residual scores",
"Mitigation actions with progress, effectiveness, evidence, and residual-risk impact",
"Deployment gate criteria, reviewer decisions, and evidence traceability",
"Indicator catalog with measurands, units, thresholds, and interpretation rules",
"Project-management mappings for Trello, Jira, Asana, and Microsoft Planner",
"Audit trail for governance and review evidence"
];
const list = $("#reportCoverageList");
if (list) list.innerHTML = coverage.map(item => `
${escapeHtml(item)} `).join("");
}
function reportUrl(reportName) {
return `/api/projects/${encodeURIComponent(state.projectId)}/reports/${encodeURIComponent(reportName)}`;
}
function openReportFromUi(reportName) {
if (!ensureApi()) return;
window.open(reportUrl(reportName), "_blank", "noopener,noreferrer");
}
async function downloadReportFromUi(reportName, filename) {
if (!ensureApi()) return;
try {
const response = await fetch(reportUrl(reportName));
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error || `Report request failed: ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
showToast("Report export prepared.");
} catch (error) {
showToast(error.message, "error");
}
}
async function downloadJsonReportFromUi(reportName, filename) {
if (!ensureApi()) return;
try {
const response = await apiJson(reportUrl(reportName));
downloadFile(filename, JSON.stringify(response.report || response, null, 2), "application/json");
showToast("JSON report export prepared.");
} catch (error) {
showToast(error.message, "error");
}
}
function renderAccessControl() {
const summary = $("#accessSummaryCards");
if (!summary) return;
const access = state.access;
if (!state.apiAvailable || !access || !access.actor) {
summary.innerHTML = `
Access control Offline Start the Node.js server to inspect roles. `;
return;
}
const actor = access.actor;
const users = access.users || [];
const roles = access.roles || [];
const permissions = actor.permissions || [];
summary.innerHTML = [
{ label: "Current actor", value: actor.displayName, detail: actor.roleName, status: actor.active ? "Pass" : "Blocked" },
{ label: "Actor permissions", value: permissions.includes("*") ? "All" : permissions.length, detail: permissions.includes("*") ? "Administrator access" : "Assigned by role", status: permissions.includes("*") ? "Pass" : "Neutral" },
{ label: "Users", value: users.length, detail: "Local prototype accounts", status: "Neutral" },
{ label: "Roles", value: roles.length, detail: "Governance permission profiles", status: "Neutral" }
].map(card => `
${escapeHtml(card.label)}
${escapeHtml(card.value)}
${escapeHtml(card.detail)}
`).join("");
const select = $("#actorUserSelect");
if (select) {
select.innerHTML = users.map(user => `
${escapeHtml(user.displayName)} · ${escapeHtml(user.roleName)}
`).join("");
}
const normalizedPermissions = permissions.includes("*") ? (access.permissions || []) : permissions;
const permissionList = $("#actorPermissionList");
if (permissionList) {
permissionList.innerHTML = normalizedPermissions.map(permission => `
${escapeHtml(permission)} `).join("") || `
No permissions assigned. `;
}
const userTable = $("#accessUserTable");
if (userTable) {
userTable.innerHTML = users.map(user => `
${escapeHtml(user.displayName)} ${escapeHtml(user.id)}
${escapeHtml(user.email)}
${escapeHtml(user.roleName)}
${user.active ? "Active" : "Inactive"}
`).join("");
}
const roleTable = $("#accessRoleTable");
if (roleTable) {
roleTable.innerHTML = roles.map(role => `
${escapeHtml(role.name)} ${escapeHtml(role.id)}
${(role.permissions || []).map(permission => `${escapeHtml(permission)} `).join(" ")}
`).join("");
}
}
function hasPermission(permission) {
const actor = state.access && state.access.actor;
if (!actor || !Array.isArray(actor.permissions)) return false;
return actor.permissions.includes("*") || actor.permissions.includes(permission);
}
function permissionHelp(permission) {
if (!state.apiAvailable) return "Start the Node.js server to enable this action.";
const actor = state.access && state.access.actor;
return actor ? `Permission required: ${permission}. Current role: ${actor.roleName}.` : `Permission required: ${permission}.`;
}
function renderSourceAlignment() {
const sourceRows = [
["Research problem", "Source-derived", "AI projects have data, model, governance, deployment, and measurement risks that are not handled well by traditional project methods."],
["Governance triangle", "Source-derived", "Organizational, technical, and human dimensions are the dashboard's main risk structure."],
["Measurement alignment", "Source-derived", "Indicators are modeled as measurands, units, interpretation rules, and evidence."],
["Software prototype and REST API", "Source-derived", "The implementation includes a working dashboard and API endpoints."],
["Dashboard scoring formula", "Implementation extension", "Probability, impact, detectability, mitigation progress, and approval penalty are proposed software logic."],
["Deployment gate", "Implementation extension", "Gate rules convert measurement results into Ready, Warning, or Blocked decisions."],
["Multi-PM integration", "Implementation extension", "Trello, Jira, Asana, and Microsoft Planner adapters support simulated sync, OAuth authorization, and live API client boundaries."]
];
$("#sourceAlignment").innerHTML = sourceRows.map(row => `
${escapeHtml(row[0])}
${escapeHtml(row[1])}
${escapeHtml(row[2])}
`).join("");
}
function openCreateRiskDialog() {
if (!ensureApi()) return;
$("#riskForm").reset();
$("#riskFormMode").value = "create";
$("#riskId").value = "";
$("#riskDialogTitle").textContent = "Add AI risk";
$("#riskDimension").value = "Technical";
$("#riskDomain").value = "Technical risks";
$("#riskLifecycle").value = "Testing and performance evaluation";
$("#riskProbability").value = "3";
$("#riskImpact").value = "4";
$("#riskDetectability").value = "3";
$("#riskStatus").value = "Open";
$("#riskApproval").value = "Pending";
$("#riskOwner").value = "Default Risk Owner";
updateRiskScorePreview();
$("#riskDialog").showModal();
}
function openEditRiskDialog(risk) {
if (!ensureApi()) return;
$("#riskForm").reset();
$("#riskFormMode").value = "edit";
$("#riskId").value = risk.id;
$("#riskDialogTitle").textContent = "Edit AI risk";
$("#riskTitle").value = risk.title || "";
$("#riskDescription").value = risk.description || "";
$("#riskDimension").value = risk.dimension || "Technical";
$("#riskDomain").value = risk.domain || "Technical risks";
setSelectValue("#riskLifecycle", risk.lifecyclePhase || "Testing and performance evaluation");
$("#riskOwner").value = risk.owner || "Unassigned";
$("#riskProbability").value = risk.probability || 3;
$("#riskImpact").value = risk.impact || 3;
$("#riskDetectability").value = risk.detectability || 3;
$("#riskStatus").value = risk.status || "Open";
$("#riskApproval").value = risk.approvalStatus || "Pending";
$("#riskDueDate").value = risk.dueDate || "";
$("#riskEvidence").value = risk.evidence || "";
updateRiskScorePreview();
$("#riskDialog").showModal();
}
function setSelectValue(selector, value) {
const select = $(selector);
if (!select) return;
if (!Array.from(select.options).some(option => option.value === value)) {
const option = document.createElement("option");
option.value = value;
option.textContent = value;
select.appendChild(option);
}
select.value = value;
}
function openCreateMitigationDialog(risk) {
if (!ensureApi()) return;
$("#mitigationForm").reset();
$("#mitigationFormMode").value = "create";
$("#mitigationId").value = "";
$("#mitigationDialogTitle").textContent = "Add mitigation action";
populateMitigationRiskSelect(risk ? risk.id : null);
const selectedRisk = risk || findRisk($("#mitigationRiskSelect").value);
$("#mitigationRiskId").value = selectedRisk ? selectedRisk.id : "";
$("#mitigationRiskLabel").textContent = selectedRisk ? `${selectedRisk.id}: ${selectedRisk.title}` : "Select the risk this mitigation treats.";
$("#mitigationOwner").value = selectedRisk ? selectedRisk.owner : "Default Risk Owner";
$("#mitigationStatus").value = "In progress";
$("#mitigationProgress").value = "50";
$("#mitigationEffectiveness").value = "60";
$("#mitigationEvidenceType").value = "Review";
$("#mitigationRiskSelect").onchange = () => {
const changedRisk = findRisk($("#mitigationRiskSelect").value);
$("#mitigationRiskId").value = changedRisk ? changedRisk.id : "";
$("#mitigationRiskLabel").textContent = changedRisk ? `${changedRisk.id}: ${changedRisk.title}` : "Select the risk this mitigation treats.";
if (changedRisk) $("#mitigationOwner").value = changedRisk.owner || "Default Risk Owner";
};
$("#mitigationDialog").showModal();
}
function openEditMitigationDialog(mitigation) {
if (!ensureApi()) return;
$("#mitigationForm").reset();
$("#mitigationFormMode").value = "edit";
$("#mitigationId").value = mitigation.id;
$("#mitigationRiskId").value = mitigation.riskId;
$("#mitigationDialogTitle").textContent = "Edit mitigation action";
populateMitigationRiskSelect(mitigation.riskId);
$("#mitigationRiskSelect").disabled = true;
$("#mitigationRiskLabel").textContent = `${mitigation.riskId}: ${mitigation.riskTitle}`;
$("#mitigationTitle").value = mitigation.title || "";
$("#mitigationDescription").value = mitigation.description || "";
$("#mitigationOwner").value = mitigation.owner || "Unassigned";
$("#mitigationStatus").value = mitigation.status || "In progress";
$("#mitigationProgress").value = mitigation.progress_percent || 0;
$("#mitigationEffectiveness").value = mitigation.effectiveness_percent || 0;
$("#mitigationDueDate").value = mitigation.dueDate || "";
$("#mitigationEvidenceType").value = "Review";
$("#mitigationEvidenceTitle").value = "";
$("#mitigationEvidenceUri").value = "";
$("#mitigationDialog").showModal();
}
function populateMitigationRiskSelect(selectedRiskId) {
const select = $("#mitigationRiskSelect");
select.disabled = false;
const risks = (state.dashboard && state.dashboard.scoredRisks || [])
.filter(risk => risk.status !== "Closed")
.sort((a, b) => b.residualScore - a.residualScore);
select.innerHTML = risks.map(risk => `
${escapeHtml(risk.id)} · ${escapeHtml(risk.title)} · residual ${risk.residualScore}/100
`).join("");
}
async function saveRiskFromUi() {
if (!ensureApi()) return;
const mode = $("#riskFormMode").value;
const riskId = $("#riskId").value;
const payload = {
title: $("#riskTitle").value.trim(),
description: $("#riskDescription").value.trim(),
dimension: $("#riskDimension").value,
domain: $("#riskDomain").value,
lifecyclePhase: $("#riskLifecycle").value,
probability: Number($("#riskProbability").value),
impact: Number($("#riskImpact").value),
detectability: Number($("#riskDetectability").value),
status: $("#riskStatus").value,
approvalStatus: $("#riskApproval").value,
dueDate: $("#riskDueDate").value || null,
owner: $("#riskOwner").value.trim() || "Unassigned",
evidence: $("#riskEvidence").value.trim()
};
try {
if (mode === "edit") {
await apiJson(`/api/risks/${encodeURIComponent(riskId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
showToast("Risk updated and dashboard recalculated.");
} else {
await apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/risks`, {
method: "POST",
body: JSON.stringify(payload)
});
showToast("Risk created and dashboard recalculated.");
}
$("#riskDialog").close();
await loadData();
} catch (error) {
showToast(error.message, "error");
}
}
async function saveMitigationFromUi() {
if (!ensureApi()) return;
const mode = $("#mitigationFormMode").value;
const mitigationId = $("#mitigationId").value;
const riskId = $("#mitigationRiskId").value || $("#mitigationRiskSelect").value;
const payload = {
title: $("#mitigationTitle").value.trim(),
description: $("#mitigationDescription").value.trim(),
owner: $("#mitigationOwner").value.trim() || "Unassigned",
status: $("#mitigationStatus").value,
progress_percent: Number($("#mitigationProgress").value),
effectiveness_percent: Number($("#mitigationEffectiveness").value),
dueDate: $("#mitigationDueDate").value || null,
evidenceType: $("#mitigationEvidenceType").value,
evidenceTitle: $("#mitigationEvidenceTitle").value.trim(),
evidenceUri: $("#mitigationEvidenceUri").value.trim()
};
try {
if (mode === "edit") {
await apiJson(`/api/mitigations/${encodeURIComponent(mitigationId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
showToast("Mitigation updated and residual risk recalculated.");
} else {
await apiJson(`/api/risks/${encodeURIComponent(riskId)}/mitigations`, {
method: "POST",
body: JSON.stringify(payload)
});
showToast("Mitigation added and residual risk recalculated.");
}
$("#mitigationDialog").close();
await loadData();
} catch (error) {
showToast(error.message, "error");
}
}
async function deleteRiskFromUi(risk) {
if (!ensureApi()) return;
const ok = window.confirm(`Delete risk ${risk.id}? This also removes linked mitigations and evidence records.`);
if (!ok) return;
try {
await apiJson(`/api/risks/${encodeURIComponent(risk.id)}`, { method: "DELETE" });
await loadData();
showToast("Risk deleted and dashboard recalculated.");
} catch (error) {
showToast(error.message, "error");
}
}
async function markMitigationDoneFromUi(mitigation) {
if (!ensureApi()) return;
try {
await apiJson(`/api/mitigations/${encodeURIComponent(mitigation.id)}`, {
method: "PATCH",
body: JSON.stringify({
title: mitigation.title,
status: "Done",
progress_percent: 100,
effectiveness_percent: Math.max(Number(mitigation.effectiveness_percent || 0), 80),
evidenceTitle: `Mitigation marked done: ${mitigation.title}`,
evidenceType: "Review"
})
});
await loadData();
showToast("Mitigation marked done and residual risk recalculated.");
} catch (error) {
showToast(error.message, "error");
}
}
async function deleteMitigationFromUi(mitigation) {
if (!ensureApi()) return;
const ok = window.confirm(`Delete mitigation ${mitigation.id}? Linked mitigation evidence records will also be removed.`);
if (!ok) return;
try {
await apiJson(`/api/mitigations/${encodeURIComponent(mitigation.id)}`, { method: "DELETE" });
await loadData();
showToast("Mitigation deleted and residual risk recalculated.");
} catch (error) {
showToast(error.message, "error");
}
}
async function evaluateGateFromUi() {
if (!ensureApi()) return;
try {
const response = await apiJson(`/api/projects/${encodeURIComponent(state.projectId)}/gate/evaluate`, { method: "POST", body: JSON.stringify({}) });
state.selectedGateId = response.persistedGate ? response.persistedGate.id : null;
await loadData();
showToast("Deployment gate evaluated and stored for reviewer decision.");
} catch (error) {
showToast(error.message, "error");
}
}
async function saveGateDecisionFromUi() {
if (!ensureApi()) return;
const gate = getSelectedGate();
if (!gate || !gate.id) return showToast("Evaluate and persist a gate before recording a reviewer decision.", "error");
const payload = {
decision: $("#gateDecisionSelect").value,
reviewerName: $("#gateReviewerName").value.trim() || "Governance Reviewer",
reason: $("#gateDecisionReason").value.trim() || "No reason recorded.",
actorUserId: state.actorUserId
};
try {
await apiJson(`/api/gates/${encodeURIComponent(gate.id)}/decisions`, {
method: "POST",
body: JSON.stringify(payload)
});
await loadData();
showToast("Reviewer gate decision recorded in the audit trail.");
} catch (error) {
showToast(error.message, "error");
}
}
async function saveGateCriteriaNotesFromUi() {
if (!ensureApi()) return;
const gate = getSelectedGate();
if (!gate || !gate.id) return showToast("Evaluate and persist a gate before saving criterion notes.", "error");
const criteriaIds = Array.from(new Set($$("[data-criterion-id]").map(item => item.getAttribute("data-criterion-id")).filter(Boolean)));
if (!criteriaIds.length) return showToast("No persisted gate criteria are available to update.", "error");
try {
await Promise.all(criteriaIds.map(criterionId => {
const statusElement = $(`.criterion-status[data-criterion-id="${cssEscape(criterionId)}"]`);
const noteElement = $(`.criterion-note[data-criterion-id="${cssEscape(criterionId)}"]`);
const evidenceElement = $(`.criterion-evidence[data-criterion-id="${cssEscape(criterionId)}"]`);
return apiJson(`/api/gate-criteria/${encodeURIComponent(criterionId)}`, {
method: "PATCH",
body: JSON.stringify({
reviewerStatus: statusElement ? statusElement.value : "Reviewed",
reviewerNote: noteElement ? noteElement.value.trim() : "",
evidenceUri: evidenceElement ? evidenceElement.value.trim() : "",
actorUserId: state.actorUserId
})
});
}));
await loadData();
showToast("Gate criterion notes and evidence links saved.");
} catch (error) {
showToast(error.message, "error");
}
}
function updateRiskScorePreview() {
const risk = {
probability: Number($("#riskProbability").value),
impact: Number($("#riskImpact").value),
detectability: Number($("#riskDetectability").value),
mitigationProgress: 0,
mitigationEffectiveness: 0,
status: $("#riskStatus").value || "Open"
};
const scored = window.CosmicRiskEngine.scoreRisk(risk);
$("#riskScorePreview").innerHTML = `
Preview: raw score ${scored.rawScore}/125, normalized score ${scored.normalizedScore}/100, severity ${scored.severity}.
Residual risk will change after mitigation actions are added.
`;
}
async function syncIntegrationFromUi(integrationId) {
if (!ensureApi()) return;
try {
const result = await apiJson(`/api/integrations/${encodeURIComponent(integrationId)}/sync`, {
method: "POST",
body: JSON.stringify({ actorUserId: state.actorUserId })
});
showToast(`${result.integration.provider} simulated sync completed: ${result.syncRun.createdCount} created, ${result.syncRun.updatedCount} updated.`);
await loadData();
} catch (error) {
showToast(error.message, "error");
}
}
async function authorizeIntegrationFromUi(integrationId) {
if (!ensureApi()) return;
const integration = (state.pmIntegrations || []).find(item => item.id === integrationId);
if (!integration) return showToast("Integration not found.", "error");
try {
const result = await apiJson(`/api/oauth/${encodeURIComponent(integration.provider)}/authorize?integrationId=${encodeURIComponent(integrationId)}&actorUserId=${encodeURIComponent(state.actorUserId)}`);
window.open(result.authorizeUrl, "_blank", "noopener,noreferrer");
showToast(`${integration.provider} authorization URL opened. Complete consent in the new window.`);
} catch (error) {
showToast(error.message, "error");
}
}
async function testLiveIntegrationFromUi(integrationId) {
if (!ensureApi()) return;
try {
const result = await apiJson(`/api/integrations/${encodeURIComponent(integrationId)}/live/test`, { method: "POST" });
if (result.dryRun) return showToast(result.message, "warning");
showToast(`${result.provider} live connection passed for ${result.account || "connected account"}.`);
await loadData();
} catch (error) {
showToast(error.message, "error");
}
}
async function syncLiveIntegrationFromUi(integrationId) {
if (!ensureApi()) return;
try {
const result = await apiJson(`/api/integrations/${encodeURIComponent(integrationId)}/live/sync`, {
method: "POST",
body: JSON.stringify({ actorUserId: state.actorUserId })
});
if (result.dryRun) {
showToast(result.message, "warning");
} else {
showToast(`${result.integration.provider} live sync completed: ${result.syncRun.createdCount} created, ${result.syncRun.updatedCount} updated, ${result.syncRun.failedCount} failed.`);
}
await loadData();
} catch (error) {
showToast(error.message, "error");
}
}
function findRisk(riskId) {
return (state.dashboard && state.dashboard.scoredRisks || []).find(risk => risk.id === riskId);
}
function findMitigation(mitigationId) {
return (state.dashboard && state.dashboard.mitigations || []).find(mitigation => mitigation.id === mitigationId);
}
function averageNumbers(values) {
const nums = values.map(Number).filter(Number.isFinite);
if (!nums.length) return 0;
return Math.round(nums.reduce((sum, value) => sum + value, 0) / nums.length);
}
function isOverdue(dateText, status) {
if (!dateText || status === "Done" || status === "Rejected") return false;
const due = new Date(`${dateText}T23:59:59`);
if (Number.isNaN(due.getTime())) return false;
const today = new Date();
today.setHours(0, 0, 0, 0);
return due < today;
}
function ensureApi() {
if (state.apiAvailable) return true;
showToast("Write actions require the Node.js server. Run node server.js, then refresh the dashboard.", "error");
return false;
}
function showError(message) {
$("main").innerHTML = `
Unable to load dashboard ${escapeHtml(message)}
`;
}
function showToast(message, type) {
const toast = $("#toast");
toast.textContent = message;
toast.className = `toast visible ${type === "error" ? "error" : ""}`;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.className = "toast";
}, 3600);
}
function downloadFile(filename, text, type) {
const blob = new Blob([text], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
function formatMetric(value) {
const num = Number(value);
return Number.isFinite(num) ? num.toFixed(2) : "Missing";
}
function formatDateTime(value) {
if (!value) return "Missing";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
}
function cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(value);
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}
function formatDateTime(value) {
if (!value) return "Not yet";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
}
function escapeAttribute(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(//g, ">");
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function statusClass(status) {
const normalized = String(status || "").toLowerCase().replace(/\s+/g, "-");
if (["pass", "ready", "accepted", "approved", "closed", "complete", "low", "done", "reviewed"].includes(normalized)) return "pass";
if (["warning", "moderate", "in-mitigation", "pending", "pending-review", "needs-changes", "evidence-requested", "not-approved", "partial", "not-started", "in-progress"].includes(normalized)) return "warning";
if (["blocked", "fail", "failed", "rejected", "critical", "open", "high"].includes(normalized)) return "blocked";
return "neutral";
}
function scoreClass(score) {
const s = Number(score) || 0;
if (s >= 75) return "blocked";
if (s >= 50) return "warning";
return "pass";
}
function dimensionClass(dimension) {
return String(dimension || "").toLowerCase();
}
})();