/home/techb158/trellopowerup.abdallabala.com/public
Edit: /home/techb158/trellopowerup.abdallabala.com/public/risk-modal.js (20746B)
import { apiFetch, canWriteCard, escapeHtml, powerUpOptions, statusClass } from './client-utils.js';
const t = window.TrelloPowerUp.iframe(powerUpOptions());
const form = document.getElementById('risk-form');
const result = document.getElementById('result');
const closeBtn = document.getElementById('close');
const checklistBtn = document.getElementById('create-checklist');
const commentBtn = document.getElementById('post-summary-comment');
const removeAssessmentBtn = document.getElementById('remove-assessment');
const templateSelect = document.getElementById('riskTemplate');
const applyTemplateBtn = document.getElementById('apply-template');
const syncChecklistBtn = document.getElementById('sync-checklist-progress');
const checklistProgressNote = document.getElementById('checklist-progress-note');
const MITIGATION_CHECKLIST_NAME = 'COSMIC Risk Mitigation';
let currentAssessment = null;
const RISK_TEMPLATES = {
data_quality: {
title: 'Training data quality is insufficient',
lifecyclePhase: 'data_preparation',
dimension: 'technical',
category: 'technical',
probability: 4,
impact: 5,
detectionDifficulty: 3,
mitigationCompleteness: 20,
modelPerformanceScore: 70,
dataReadinessScore: 55,
ethicalReviewCompleted: false,
legalReviewCompleted: false,
mitigation: 'Validate source data, define data quality thresholds, document missingness and bias checks, and attach evidence before model training continues.'
},
model_drift: {
title: 'Model drift may reduce production performance',
lifecyclePhase: 'deployment_monitoring',
dimension: 'technical',
category: 'technical',
probability: 3,
impact: 5,
detectionDifficulty: 4,
mitigationCompleteness: 25,
modelPerformanceScore: 68,
dataReadinessScore: 75,
ethicalReviewCompleted: true,
legalReviewCompleted: true,
mitigation: 'Define drift indicators, monitor production data distribution, set retraining triggers, and document rollback criteria.'
},
bias_fairness: {
title: 'Bias or unfair outcome risk is unresolved',
lifecyclePhase: 'testing_evaluation',
dimension: 'human',
category: 'legal_ethical',
probability: 3,
impact: 5,
detectionDifficulty: 4,
mitigationCompleteness: 15,
modelPerformanceScore: 72,
dataReadinessScore: 70,
ethicalReviewCompleted: false,
legalReviewCompleted: false,
mitigation: 'Run subgroup performance analysis, document fairness criteria, review affected stakeholder groups, and record ethics approval evidence.'
},
deployment_rollback: {
title: 'Deployment rollback plan is incomplete',
lifecyclePhase: 'deployment_monitoring',
dimension: 'organizational',
category: 'strategic_organizational',
probability: 3,
impact: 4,
detectionDifficulty: 3,
mitigationCompleteness: 30,
modelPerformanceScore: 76,
dataReadinessScore: 80,
ethicalReviewCompleted: true,
legalReviewCompleted: true,
mitigation: 'Assign rollback owner, document rollback decision criteria, test rollback workflow, and attach deployment runbook evidence.'
},
legal_ethics: {
title: 'Legal and ethical review is not complete',
lifecyclePhase: 'design',
dimension: 'organizational',
category: 'legal_ethical',
probability: 3,
impact: 5,
detectionDifficulty: 3,
mitigationCompleteness: 10,
modelPerformanceScore: 70,
dataReadinessScore: 70,
ethicalReviewCompleted: false,
legalReviewCompleted: false,
mitigation: 'Identify applicable legal obligations, complete ethics review, document residual risk acceptance, and attach approval evidence.'
}
};
const COSMIC_LABELS = {
'Risk: Low': 'green',
'Risk: Medium': 'yellow',
'Risk: High': 'orange',
'Risk: Critical': 'red',
'Gate: Ready': 'sky',
'Gate: Blocked': 'black'
};
const COSMIC_LABEL_NAMES = Object.keys(COSMIC_LABELS);
function field(id) {
return document.getElementById(id);
}
function numberValue(id) {
return Number(field(id).value);
}
function textValue(id) {
return field(id).value.trim();
}
function selectValue(id) {
return field(id).value;
}
function checked(id) {
return field(id).checked;
}
function setValue(id, value) {
if (value === undefined || value === null) return;
field(id).value = value;
}
function setChecked(id, value) {
if (value === undefined || value === null) return;
field(id).checked = Boolean(value);
}
function setChecklistProgressNote(message, tone = '') {
checklistProgressNote.textContent = message;
checklistProgressNote.className = `muted small ${tone}`.trim();
}
function syncRemoveButtonState() {
removeAssessmentBtn.disabled = !currentAssessment;
}
function applyRiskTemplate() {
const template = RISK_TEMPLATES[templateSelect.value];
if (!template) return;
setValue('title', template.title);
setValue('lifecyclePhase', template.lifecyclePhase);
setValue('dimension', template.dimension);
setValue('category', template.category);
setValue('probability', template.probability);
setValue('impact', template.impact);
setValue('detectionDifficulty', template.detectionDifficulty);
setValue('mitigationCompleteness', template.mitigationCompleteness);
setValue('mitigation', template.mitigation);
setValue('modelPerformanceScore', template.modelPerformanceScore);
setValue('dataReadinessScore', template.dataReadinessScore);
setChecked('ethicalReviewCompleted', template.ethicalReviewCompleted);
setChecked('legalReviewCompleted', template.legalReviewCompleted);
setValue('approvalState', 'draft');
result.hidden = true;
}
function commentTrendText(assessment) {
if (!assessment || assessment.scoreDelta === null || assessment.scoreDelta === undefined) return 'First saved assessment.';
if (assessment.scoreDelta > 0) return `Risk score worsened by ${assessment.scoreDelta} points.`;
if (assessment.scoreDelta < 0) return `Risk score improved by ${Math.abs(assessment.scoreDelta)} points.`;
return 'Risk score is unchanged.';
}
function trendText(assessment) {
if (!assessment || assessment.scoreDelta === null || assessment.scoreDelta === undefined) return 'First saved assessment';
if (assessment.scoreDelta > 0) return `Worse by ${assessment.scoreDelta} points`;
if (assessment.scoreDelta < 0) return `Improved by ${Math.abs(assessment.scoreDelta)} points`;
return 'No score change';
}
function trendClass(assessment) {
return assessment?.trend === 'improved' ? 'trend-down' : assessment?.trend === 'worse' ? 'trend-up' : '';
}
function labelStatusText(labelSyncStatus) {
if (!labelSyncStatus) return '';
if (labelSyncStatus === 'synced') return '
Labels synced';
if (labelSyncStatus === 'disabled') return '
Label sync off';
return '
Label sync failed';
}
function buildPayload() {
return {
projectTypology: selectValue('projectTypology'),
lifecyclePhase: selectValue('lifecyclePhase'),
measurementMaturity: selectValue('measurementMaturity'),
approvalState: selectValue('approvalState'),
metrics: {
modelPerformanceScore: numberValue('modelPerformanceScore'),
dataReadinessScore: numberValue('dataReadinessScore'),
ethicalReviewCompleted: checked('ethicalReviewCompleted'),
legalReviewCompleted: checked('legalReviewCompleted')
},
risks: [{
title: textValue('title'),
dimension: selectValue('dimension'),
category: selectValue('category'),
lifecyclePhase: selectValue('lifecyclePhase'),
probability: numberValue('probability'),
impact: numberValue('impact'),
detectionDifficulty: numberValue('detectionDifficulty'),
mitigationCompleteness: numberValue('mitigationCompleteness'),
mitigation: textValue('mitigation'),
approvalState: selectValue('approvalState')
}]
};
}
function renderScore(score, assessment = null, labelSyncStatus = '') {
const reasons = score.gate?.blockingReasons || [];
result.hidden = false;
result.innerHTML = `
${score.score}
${escapeHtml(score.status)}
${score.gate?.deploymentReady ? 'Gate ready' : 'Gate blocked'}
${escapeHtml(trendText(assessment))}
${assessment?.review?.needed ? '
Review needed' : ''}
${labelStatusText(labelSyncStatus)}
${escapeHtml(score.interpretation)}
${assessment?.review?.needed ? `
Review: ${escapeHtml((assessment.review.reasons || []).join('; '))}
` : ''}
${assessment?.review?.actions?.length ? `
Next action: ${escapeHtml(assessment.review.actions.join('; '))}
` : ''}
${reasons.length ? `
${reasons.map((reason) => `- ${escapeHtml(reason)}
`).join('')}
` : ''}
`;
}
function applyBoardDefaults(settings = {}) {
setValue('projectTypology', settings.defaultTypology || 'ai_enabler');
setValue('lifecyclePhase', settings.defaultLifecyclePhase || 'model_development');
setValue('measurementMaturity', settings.defaultMeasurementMaturity || 'defined');
}
function fillForm(input = {}) {
const risk = Array.isArray(input.risks) ? input.risks[0] : null;
const metrics = input.metrics || {};
setValue('projectTypology', input.projectTypology);
setValue('lifecyclePhase', input.lifecyclePhase || risk?.lifecyclePhase);
setValue('measurementMaturity', input.measurementMaturity);
setValue('approvalState', input.approvalState || risk?.approvalState);
setValue('modelPerformanceScore', metrics.modelPerformanceScore);
setValue('dataReadinessScore', metrics.dataReadinessScore);
setChecked('ethicalReviewCompleted', metrics.ethicalReviewCompleted);
setChecked('legalReviewCompleted', metrics.legalReviewCompleted);
if (!risk) return;
setValue('title', risk.title);
setValue('dimension', risk.dimension);
setValue('category', risk.category);
setValue('probability', risk.probability);
setValue('impact', risk.impact);
setValue('detectionDifficulty', risk.detectionDifficulty);
setValue('mitigationCompleteness', risk.mitigationCompleteness);
setValue('mitigation', risk.mitigation);
}
async function loadLatestAssessment() {
const card = await t.card('id');
const board = await t.board('id');
const { assessment } = await apiFetch(t, `/api/boards/${board.id}/cards/${card.id}/assessments/latest`);
if (!assessment) return;
currentAssessment = assessment;
fillForm(assessment.input);
renderScore(assessment.score, assessment);
syncRemoveButtonState();
}
async function trelloFetch(path, token, options = {}) {
const key = window.COSMIC_TRELLO_API_KEY || 'replace_with_power_up_api_key';
const separator = path.includes('?') ? '&' : '?';
const response = await fetch(`https://api.trello.com/1${path}${separator}key=${encodeURIComponent(key)}&token=${encodeURIComponent(token)}`, options);
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Trello request failed: ${response.status}`);
}
return response.status === 204 ? null : response.json();
}
async function getAuthorizedToken() {
const restApi = t.getRestApi();
const authorized = await restApi.isAuthorized();
if (!authorized) {
await restApi.authorize({ scope: 'read,write' });
}
return restApi.getToken();
}
async function findOrCreateBoardLabel(boardId, token, name) {
const labels = await trelloFetch(`/boards/${boardId}/labels?limit=1000`, token);
const existing = labels.find((label) => label.name === name);
if (existing) return existing;
return trelloFetch(`/labels`, token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idBoard: boardId, name, color: COSMIC_LABELS[name] })
});
}
async function syncCosmicLabels(cardId, boardId, score) {
const token = await getAuthorizedToken();
const cardLabels = await trelloFetch(`/cards/${cardId}/labels`, token);
const activeLabels = [
`Risk: ${score.status.charAt(0).toUpperCase()}${score.status.slice(1)}`,
score.gate?.deploymentReady ? 'Gate: Ready' : 'Gate: Blocked'
];
for (const label of cardLabels.filter((item) => COSMIC_LABEL_NAMES.includes(item.name))) {
await trelloFetch(`/cards/${cardId}/idLabels/${label.id}`, token, { method: 'DELETE' });
}
for (const name of activeLabels) {
const label = await findOrCreateBoardLabel(boardId, token, name);
await trelloFetch(`/cards/${cardId}/idLabels`, token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: label.id })
});
}
}
async function syncLabelsIfEnabled(cardId, boardId, score) {
const settings = await t.get('board', 'shared', 'cosmicSettings').catch(() => null);
if (settings?.labelSyncEnabled !== true) return 'disabled';
await syncCosmicLabels(cardId, boardId, score);
return 'synced';
}
async function syncMitigationCompletenessFromChecklist() {
const writable = await canWriteCard(t);
if (!writable) throw new Error('You do not have permission to read this card checklist.');
const token = await getAuthorizedToken();
const context = await t.getContext();
const checklists = await trelloFetch(`/cards/${context.card}/checklists`, token);
const checklist = checklists.find((item) => item.name.toLowerCase() === MITIGATION_CHECKLIST_NAME.toLowerCase());
if (!checklist) {
setChecklistProgressNote(`No ${MITIGATION_CHECKLIST_NAME} checklist found. Create one first.`, 'warning-text');
return;
}
const items = checklist.checkItems || [];
if (!items.length) {
setValue('mitigationCompleteness', 0);
setChecklistProgressNote(`${MITIGATION_CHECKLIST_NAME} has no checklist items yet.`, 'warning-text');
return;
}
const complete = items.filter((item) => item.state === 'complete').length;
const progress = Math.round((complete / items.length) * 100);
setValue('mitigationCompleteness', progress);
setChecklistProgressNote(`Synced mitigation completeness from checklist: ${complete} of ${items.length} items complete (${progress}%).`, progress >= 60 ? 'success-text' : 'warning-text');
result.hidden = true;
}
async function saveRisk(event) {
event.preventDefault();
const writable = await canWriteCard(t);
if (!writable) throw new Error('You do not have permission to write to this card.');
const payload = buildPayload();
const card = await t.card('id', 'name');
const board = await t.board('id', 'name');
const assessment = await apiFetch(t, `/api/boards/${board.id}/cards/${card.id}/assessments`, {
method: 'POST',
body: JSON.stringify(payload)
});
currentAssessment = assessment;
syncRemoveButtonState();
await t.set('card', 'shared', 'cosmicRisk', assessment.score);
await t.set('card', 'shared', 'cosmicRiskAssessmentId', assessment.id);
await t.set('card', 'shared', 'cosmicRiskReview', assessment.review || null);
let labelSyncStatus = 'disabled';
try {
labelSyncStatus = await syncLabelsIfEnabled(card.id, board.id, assessment.score);
} catch (error) {
labelSyncStatus = 'failed';
console.warn('COSMIC label sync failed:', error);
}
renderScore(assessment.score, assessment, labelSyncStatus);
}
async function removeAssessment() {
if (!currentAssessment) return;
const confirmed = confirm('Remove all COSMIC AI-Risk assessments for this card? This clears the dashboard entry, badges, and card section for this card.');
if (!confirmed) return;
const writable = await canWriteCard(t);
if (!writable) throw new Error('You do not have permission to update this card.');
removeAssessmentBtn.disabled = true;
const card = await t.card('id');
const board = await t.board('id');
await apiFetch(t, `/api/boards/${board.id}/cards/${card.id}/assessments`, { method: 'DELETE' });
await Promise.all([
t.remove('card', 'shared', 'cosmicRisk'),
t.remove('card', 'shared', 'cosmicRiskAssessmentId'),
t.remove('card', 'shared', 'cosmicRiskReview')
]);
currentAssessment = null;
result.hidden = false;
result.innerHTML = '
COSMIC AI-Risk assessment removed from this card.
';
syncRemoveButtonState();
}
function buildAssessmentComment(assessment) {
const score = assessment.score || {};
const risk = score.highestRisk || assessment.input?.risks?.[0] || {};
const blockers = score.gate?.blockingReasons || [];
const lines = [
'**COSMIC AI-Risk Assessment Summary**',
`- Score: ${score.score}/100 (${score.status})`,
`- Deployment gate: ${score.gate?.deploymentReady ? 'Ready' : 'Blocked'}`,
`- Trend: ${commentTrendText(assessment)}`,
`- Highest risk: ${risk.title || 'Not recorded'}`,
`- Governance dimension: ${risk.dimension || 'Not recorded'}`,
`- Mitigation completeness: ${score.averageMitigationCompleteness ?? 'Not recorded'}%`
];
if (risk.mitigation) lines.push(`- Mitigation plan: ${risk.mitigation}`);
if (blockers.length) {
lines.push('', 'Blocking criteria:');
for (const reason of blockers) lines.push(`- ${reason}`);
}
lines.push('', `Assessment ID: ${assessment.id}`);
return lines.join('\n');
}
async function postAssessmentSummaryComment() {
if (!currentAssessment) {
const card = await t.card('id');
const board = await t.board('id');
const { assessment } = await apiFetch(t, `/api/boards/${board.id}/cards/${card.id}/assessments/latest`);
currentAssessment = assessment;
syncRemoveButtonState();
}
if (!currentAssessment) throw new Error('Save an assessment before posting a summary comment.');
const writable = await canWriteCard(t);
if (!writable) throw new Error('You do not have permission to comment on this card.');
const token = await getAuthorizedToken();
const context = await t.getContext();
await trelloFetch(`/cards/${context.card}/actions/comments`, token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: buildAssessmentComment(currentAssessment) })
});
alert('COSMIC assessment summary comment posted.');
}
async function createMitigationChecklist() {
const writable = await canWriteCard(t);
if (!writable) throw new Error('You do not have permission to write to this card.');
const token = await getAuthorizedToken();
const context = await t.getContext();
const checklist = await trelloFetch(`/cards/${context.card}/checklists`, token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'COSMIC Risk Mitigation' })
});
const items = [
'Define mitigation owner',
'Attach evidence for data readiness',
'Attach model performance evaluation',
'Complete ethical review',
'Complete legal review',
'Record residual risk approval'
];
for (const name of items) {
await trelloFetch(`/checklists/${checklist.id}/checkItems`, token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
}
alert('Mitigation checklist created.');
}
applyTemplateBtn.addEventListener('click', applyRiskTemplate);
syncChecklistBtn.addEventListener('click', () => syncMitigationCompletenessFromChecklist().catch((error) => alert(error.message)));
form.addEventListener('submit', (event) => saveRisk(event).catch((error) => alert(error.message)));
checklistBtn.addEventListener('click', () => createMitigationChecklist().catch((error) => alert(error.message)));
commentBtn.addEventListener('click', () => postAssessmentSummaryComment().catch((error) => alert(error.message)));
removeAssessmentBtn.addEventListener('click', () => removeAssessment().catch((error) => { syncRemoveButtonState(); alert(error.message); }));
closeBtn.addEventListener('click', () => t.closeModal());
t.render(() => loadLatestAssessment().catch(() => Promise.resolve()).finally(syncRemoveButtonState));