/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/board-dashboard.js (13719B)
import { apiFetch, escapeHtml, getApiBase, powerUpOptions, statusClass } from './client-utils.js'; const t = window.TrelloPowerUp.iframe(powerUpOptions()); const summary = document.getElementById('summary'); const reviewQueueNode = document.getElementById('review-queue'); const assessmentsNode = document.getElementById('assessments'); const exportJsonBtn = document.getElementById('export-json'); const exportCsvBtn = document.getElementById('export-csv'); const searchInput = document.getElementById('search'); const statusFilter = document.getElementById('status-filter'); const gateFilter = document.getElementById('gate-filter'); const trendFilter = document.getElementById('trend-filter'); const reviewFilter = document.getElementById('review-filter'); const filterSummary = document.getElementById('filter-summary'); let currentBoardId = null; let currentData = null; let currentReviewQueue = null; function metric(title, value, note = '') { return `

${escapeHtml(title)}

${escapeHtml(value)}

${escapeHtml(note)}

`; } function scoreCounts(assessments = []) { return assessments.reduce((counts, item) => { const status = item.score?.status || 'unknown'; counts[status] = (counts[status] || 0) + 1; return counts; }, {}); } function statusMetrics(assessments = []) { const counts = scoreCounts(assessments); return ['low', 'medium', 'high', 'critical'].map((status) => { return `${status}: ${counts[status] || 0}`; }).join(''); } function formatDate(value) { if (!value) return 'Not assessed'; return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)); } function formatMeta(value) { return String(value || 'not set').replaceAll('_', ' '); } function trendText(assessment) { if (!assessment || assessment.scoreDelta === null || assessment.scoreDelta === undefined) return 'First assessment'; if (assessment.scoreDelta > 0) return `Worse by ${assessment.scoreDelta}`; if (assessment.scoreDelta < 0) return `Improved by ${Math.abs(assessment.scoreDelta)}`; return 'No change'; } function trendClass(assessment) { return assessment?.trend === 'improved' ? 'trend-down' : assessment?.trend === 'worse' ? 'trend-up' : ''; } function reviewText(item) { if (!item.review?.needed) return 'No review flag'; return item.review.reasons?.join('; ') || 'Review needed'; } function reviewBadge(item) { if (!item.review?.needed) return ''; return `Review needed`; } function reviewActionsText(item) { return item.review?.actions?.join('; ') || ''; } function totalScoreDelta(assessments = []) { return assessments.reduce((sum, item) => sum + (Number(item.scoreDelta) || 0), 0); } function latestAssessmentDate(assessments = []) { const timestamps = assessments .map((item) => new Date(item.createdAt).getTime()) .filter((value) => Number.isFinite(value)); return timestamps.length ? new Date(Math.max(...timestamps)).toISOString() : null; } function trendSummary(data) { const assessments = data.latestAssessments || []; const newCount = assessments.filter((item) => item.trend === 'new').length; const delta = totalScoreDelta(assessments); const deltaText = delta > 0 ? `+${delta}` : String(delta); const lastAssessed = latestAssessmentDate(assessments); return `

Board trend snapshot

Improved: ${data.improved || 0} Worsened: ${data.worsened || 0} Unchanged: ${data.unchanged || 0} New: ${newCount} Review needed: ${data.reviewNeeded || 0} Stale: ${data.staleAssessments || 0}

Net latest score movement: ${escapeHtml(deltaText)} points across cards with prior assessments.

Last assessed: ${escapeHtml(formatDate(lastAssessed))}. Stale threshold: ${escapeHtml(data.reviewStaleDays || 30)} days.

`; } function riskTitle(item) { return item.score?.highestRisk?.title || item.input?.risks?.[0]?.title || ''; } function searchableText(item) { return [ item.cardId, riskTitle(item), item.input?.projectTypology, item.input?.lifecyclePhase, item.input?.measurementMaturity, item.score?.status, item.trend, reviewText(item), item.score?.interpretation, ...(item.score?.gate?.blockingReasons || []) ].join(' ').toLowerCase(); } function filteredAssessments(data) { const query = searchInput.value.trim().toLowerCase(); const status = statusFilter.value; const gate = gateFilter.value; const trend = trendFilter.value; const review = reviewFilter.value; return (data.latestAssessments || []).filter((item) => { if (query && !searchableText(item).includes(query)) return false; if (status !== 'all' && item.score?.status !== status) return false; if (trend !== 'all' && item.trend !== trend) return false; if (review === 'needed' && item.review?.needed !== true) return false; if (review === 'stale' && item.review?.stale !== true) return false; if (review === 'clear' && item.review?.needed === true) return false; if (gate === 'ready' && item.score?.gate?.deploymentReady !== true) return false; if (gate === 'blocked' && item.score?.gate?.deploymentReady !== false) return false; return true; }); } function renderSummary(data) { summary.innerHTML = [ metric('Cards assessed', data.cardCount, 'Cards with at least one COSMIC assessment'), metric('Average score', `${data.averageScore}/100`, 'Latest score per card'), metric('Deployment blocked', data.deploymentBlocked, 'Cards failing one or more gate criteria'), metric('Review needed', data.reviewNeeded || 0, 'Blocked, worsened, or stale assessments'), metric('Improved', data.improved || 0, 'Cards with a lower latest score'), metric('Worsened', data.worsened || 0, 'Cards with a higher latest score') ].join(''); } function renderReviewQueue(queue) { currentReviewQueue = queue; const items = queue?.items || []; if (!items.length) { reviewQueueNode.innerHTML = '

No cards currently need review.

'; return; } reviewQueueNode.innerHTML = `
${items.length} cards need review Sorted by blocked gate, worsened score, then stale age.
${items.slice(0, 5).map((item, index) => { const score = item.score || {}; return `
${index + 1}. Card ${escapeHtml(item.cardId)} ${escapeHtml(score.status)} ${escapeHtml(score.score)}/100 ${escapeHtml(trendText(item))}

${escapeHtml(riskTitle(item) || 'Assessment')}

${escapeHtml(reviewText(item))}

${reviewActionsText(item) ? `

Next action: ${escapeHtml(reviewActionsText(item))}

` : ''}
`; }).join('')}
${items.length > 5 ? `

${items.length - 5} additional cards are available through the Review filter.

` : ''} `; document.getElementById('export-review-queue')?.addEventListener('click', () => downloadReviewQueueExport().catch((error) => alert(error.message))); document.getElementById('show-review-needed')?.addEventListener('click', () => { reviewFilter.value = 'needed'; rerenderFilters(); document.getElementById('latest-assessments')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); } function renderAssessments(data) { if (!data.latestAssessments?.length) { filterSummary.textContent = ''; assessmentsNode.innerHTML = '

No assessments have been saved for this board yet.

'; return; } const filtered = filteredAssessments(data); const sorted = [...filtered].sort((a, b) => { const reviewDelta = Number(b.review?.needed || false) - Number(a.review?.needed || false); return reviewDelta || (b.score?.score || 0) - (a.score?.score || 0); }); filterSummary.textContent = `Showing ${filtered.length} of ${data.latestAssessments.length} assessed cards.`; if (!sorted.length) { assessmentsNode.innerHTML = ` ${trendSummary(data)}

Risk distribution

${statusMetrics(data.latestAssessments)}

No assessments match the current filters.

`; return; } assessmentsNode.innerHTML = ` ${trendSummary(data)}

Risk distribution

${statusMetrics(data.latestAssessments)}
${sorted.map((item) => { const score = item.score; const blockers = score.gate?.blockingReasons || []; return `
Card ${escapeHtml(item.cardId)} ${escapeHtml(score.status)} ${score.score}/100 ${score.gate?.deploymentReady ? 'Gate ready' : 'Gate blocked'} ${escapeHtml(trendText(item))} ${reviewBadge(item)}

${escapeHtml(riskTitle(item) || 'Assessment')}

${escapeHtml(score.interpretation)}

Last assessed: ${escapeHtml(formatDate(item.createdAt))} Age: ${escapeHtml(item.review?.ageDays ?? 'n/a')} days Typology: ${escapeHtml(formatMeta(item.input?.projectTypology))} Phase: ${escapeHtml(formatMeta(item.input?.lifecyclePhase))} Maturity: ${escapeHtml(formatMeta(item.input?.measurementMaturity))}
${item.review?.needed ? `

Review: ${escapeHtml(reviewText(item))}

` : ''} ${reviewActionsText(item) ? `

Next action: ${escapeHtml(reviewActionsText(item))}

` : ''} ${blockers.length ? `

Blocked by: ${escapeHtml(blockers.slice(0, 3).join('; '))}${blockers.length > 3 ? '...' : ''}

` : ''}
`; }).join('')}`; } function render(data, reviewQueue = currentReviewQueue) { currentData = data; renderSummary(data); renderReviewQueue(reviewQueue); renderAssessments(data); } function rerenderFilters() { if (currentData) renderAssessments(currentData); } async function downloadReviewQueueExport() { if (!currentBoardId) return; const base = await getApiBase(t); const response = await fetch(`${base}/api/boards/${currentBoardId}/review-queue.csv`); if (!response.ok) throw new Error(`Review queue export failed: ${response.status}`); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `cosmic-review-queue-${currentBoardId}.csv`; document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); } async function downloadBoardExport(format) { if (!currentBoardId) return; const base = await getApiBase(t); const response = await fetch(`${base}/api/boards/${currentBoardId}/export.${format}`); if (!response.ok) throw new Error(`Export failed: ${response.status}`); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `cosmic-risk-${currentBoardId}.${format}`; document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); } async function load() { const board = await t.board('id', 'name'); currentBoardId = board.id; const [data, reviewQueue] = await Promise.all([ apiFetch(t, `/api/boards/${board.id}/summary`), apiFetch(t, `/api/boards/${board.id}/review-queue`) ]); render(data, reviewQueue); } exportJsonBtn.addEventListener('click', () => downloadBoardExport('json').catch((error) => alert(error.message))); exportCsvBtn.addEventListener('click', () => downloadBoardExport('csv').catch((error) => alert(error.message))); searchInput.addEventListener('input', rerenderFilters); statusFilter.addEventListener('change', rerenderFilters); gateFilter.addEventListener('change', rerenderFilters); trendFilter.addEventListener('change', rerenderFilters); reviewFilter.addEventListener('change', rerenderFilters); t.render(() => load().catch((error) => { assessmentsNode.innerHTML = `

${escapeHtml(error.message)}

`; }));