/home/techb158/trellopowerup.abdallabala.com/public
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)}
`;
}));