/home/techb158/trellopowerup.abdallabala.com/public
NameSizeModeActions
assets/-0755rm
.htaccess5330644editdlrm
admin.php331100644editdlrm
authorize.html10090644editdlrm
board-dashboard.html29620644editdlrm
board-dashboard.js137190644editdlrm
card-section.html5330644editdlrm
card-section.js61420644editdlrm
client-utils.js15910644editdlrm
index.html5570644editdlrm
index.php71400644editdlrm
powerup.js41850644editdlrm
risk-modal.html68150644editdlrm
risk-modal.js207460644editdlrm
settings.html25000644editdlrm
settings.js16700644editdlrm
styles.css40340644editdlrm
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 ? `` : ''} `; } 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));