Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "1.0.0",
"description": "PR Readiness Checker - Track and monitor GitHub Pull Request status",
"scripts": {
"dev": "wrangler dev",
"fingerprint": "node scripts/fingerprint.js",
"dev": "npm run fingerprint && wrangler dev",
"deploy": "wrangler deploy",
"db:create": "wrangler d1 create pr_tracker",
"db:migrate:local": "wrangler d1 migrations apply pr_tracker --local",
Expand Down
5 changes: 5 additions & 0 deletions public/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"error-reporter.js": "error-reporter.71a8b085.js",
"how-it-works.js": "how-it-works.3ebd3397.js",
"theme.js": "theme.196e3e21.js"
}
10 changes: 5 additions & 5 deletions public/diagnostics.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
}
})();
</script>
<script src="error-reporter.js"></script>
<script src="error-reporter.71a8b085.js"></script>

<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
Expand Down Expand Up @@ -55,10 +55,10 @@
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100">
<i class="fab fa-github"></i> GitHub
</a>
<button id="themeToggle"
<button data-theme-toggle
class="rounded-lg p-2 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700 transition-colors"
title="Toggle dark/light mode">
<i class="fas fa-moon" id="themeIcon"></i>
<i class="fas fa-moon" data-theme-icon></i>
</button>
</nav>
</div>
Expand Down Expand Up @@ -168,8 +168,8 @@ <h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-2 flex items-cen
buildList();

// Theme toggle
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const themeToggle = document.querySelector('[data-theme-toggle]');
const themeIcon = document.querySelector('[data-theme-icon]');
themeToggle.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
themeIcon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
Expand Down
118 changes: 118 additions & 0 deletions public/error-reporter.71a8b085.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Global error reporter: forwards JS errors to the backend,
// which relays them to the configured SLACK_ERROR_WEBHOOK.
(function () {
function safeTruncate(text, maxLen) {
var str = String(text || '');
if (str.length <= maxLen) return str;
return str.slice(0, Math.max(0, maxLen - 1)) + '…';
}

function redactSensitive(text) {
var str = String(text || '');
// Redact obvious key/value secrets and auth-like tokens.
str = str.replace(/(authorization|token|apikey|api[_-]?key|password|passwd|cookie|set-cookie|session|secret|bearer)\s*[:=]\s*([^\s,;]+)/gi, '$1:[redacted]');
// Redact email addresses.
str = str.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]');
// Redact long token-like strings.
str = str.replace(/\b[A-Za-z0-9_\-]{24,}\b/g, '[redacted-token]');
return str;
}

function sendPayload(payload) {
try {
var body = JSON.stringify(payload);

// Prefer sendBeacon (good for unload), fallback to fetch if beacon fails.
var ok = false;
try {
ok = navigator.sendBeacon('/api/client-error', body);
} catch (e) {
ok = false;
}

if (!ok) {
fetch('/api/client-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
keepalive: true,
}).catch(function () { });
}
} catch (e) { /* ignore reporting failures */ }
}

function reportError(errorType, message, stack, extra) {
var payload = Object.assign(
{ error_type: errorType, message: message, stack: stack || '' },
extra || {}
);
sendPayload(payload);
}

// 1) Catch runtime errors + resource loading errors (capture=true is important for resources)
window.addEventListener('error', function (event) {
// Resource errors (script/img/link) often have event.target and no event.error
var target = event.target || {};
var resourceUrl = target.src || target.href;

if (resourceUrl && (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG')) {
reportError(
'ResourceError',
'Failed to load or execute resource',
(event.error && event.error.stack) || '',
{ url: location.href, resource: resourceUrl }
);
return;
}

// Normal runtime error (ReferenceError/TypeError/etc.)
reportError(
(event.error && event.error.name) || 'Error',
event.message || (event.error && event.error.message) || String(event.error) || 'Unknown error',
(event.error && event.error.stack) || '',
{ url: location.href, line: event.lineno, col: event.colno }
);
}, true);

// 2) Catch unhandled promise rejections
window.addEventListener('unhandledrejection', function (event) {
var reason = event.reason || {};
reportError(
(reason.name) || 'UnhandledRejection',
reason.message || String(reason),
reason.stack || '',
{ url: location.href }
);
});

// 3) Forward handled errors that are logged via console.error
// This helps when "real errors" are caught by try/catch and only logged.
(function hookConsoleError() {
var original = console.error;
console.error = function () {
try {
var args = Array.prototype.slice.call(arguments);
var errors = args.filter(function (a) { return a instanceof Error; });

if (errors.length > 0) {
var primary = errors[0];
var message = redactSensitive(primary.name + ': ' + (primary.message || ''));
var stack = redactSensitive(primary.stack || '');
reportError(
primary.name || 'ConsoleError',
safeTruncate(message, 300),
safeTruncate(stack || message, 2000),
{
url: location.href,
source: 'console.error:unhandled',
report_channel: 'dedupe-candidate',
error_count: String(errors.length),
}
);
}
} catch (e) { /* ignore */ }

return original.apply(console, arguments);
};
})();
})();
49 changes: 34 additions & 15 deletions public/error-reporter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// Global error reporter: forwards JS errors to the backend,
// which relays them to the configured SLACK_ERROR_WEBHOOK.
(function () {
function safeTruncate(text, maxLen) {
var str = String(text || '');
if (str.length <= maxLen) return str;
return str.slice(0, Math.max(0, maxLen - 1)) + '…';
}

function redactSensitive(text) {
var str = String(text || '');
// Redact obvious key/value secrets and auth-like tokens.
str = str.replace(/(authorization|token|apikey|api[_-]?key|password|passwd|cookie|set-cookie|session|secret|bearer)\s*[:=]\s*([^\s,;]+)/gi, '$1:[redacted]');
// Redact email addresses.
str = str.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]');
// Redact long token-like strings.
str = str.replace(/\b[A-Za-z0-9_\-]{24,}\b/g, '[redacted-token]');
return str;
}

function sendPayload(payload) {
try {
var body = JSON.stringify(payload);
Expand Down Expand Up @@ -75,22 +92,24 @@
console.error = function () {
try {
var args = Array.prototype.slice.call(arguments);
var msg = args.map(function (a) {
if (a instanceof Error) {
return (a.name + ': ' + a.message + '\n' + (a.stack || ''));
}
if (typeof a === 'object') {
try { return JSON.stringify(a); } catch (_) { return '[object]'; }
}
return String(a);
}).join(' ');
var errors = args.filter(function (a) { return a instanceof Error; });

reportError(
'ConsoleError',
msg.slice(0, 300),
msg.slice(0, 2000),
{ url: location.href, source: 'console.error' }
);
if (errors.length > 0) {
var primary = errors[0];
var message = redactSensitive(primary.name + ': ' + (primary.message || ''));
var stack = redactSensitive(primary.stack || '');
reportError(
primary.name || 'ConsoleError',
safeTruncate(message, 300),
safeTruncate(stack || message, 2000),
{
url: location.href,
source: 'console.error:unhandled',
report_channel: 'dedupe-candidate',
error_count: String(errors.length),
}
);
}
} catch (e) { /* ignore */ }

return original.apply(console, arguments);
Expand Down
123 changes: 123 additions & 0 deletions public/how-it-works.3ebd3397.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Interactive Score Calculator for BLT-Leaf "How It Works" Page

function calculateScore() {
// Get input values
const ciPassed = parseInt(document.getElementById('ciPassed').value);
const ciFailed = parseInt(document.getElementById('ciFailed').value);
const approvals = parseInt(document.getElementById('approvals').value);
const changesRequested = parseInt(document.getElementById('changesRequested').value);
const conversations = parseInt(document.getElementById('conversations').value);
const responseRate = parseInt(document.getElementById('responseRate').value);

// Update displayed values
document.getElementById('ciPassedValue').textContent = ciPassed;
document.getElementById('ciFailedValue').textContent = ciFailed;
document.getElementById('approvalsValue').textContent = approvals;
document.getElementById('changesRequestedValue').textContent = changesRequested;
document.getElementById('conversationsValue').textContent = conversations;
document.getElementById('responseRateValue').textContent = responseRate + '%';

// Calculate CI Score
const totalChecks = ciPassed + ciFailed;
let ciScore = 0;
let ciConfidence = 'Low';
let ciConfidenceBadge = 'badge-low';

if (totalChecks > 0) {
ciScore = (ciPassed / totalChecks) * 100;
ciConfidence = 'High';
ciConfidenceBadge = 'badge-high';
} else {
// No checks run - low confidence, assume failure
ciScore = 0;
ciConfidence = 'Low';
ciConfidenceBadge = 'badge-low';
}

// Calculate Review Score
let reviewScore = 0;
let reviewConfidence = 'Medium';
let reviewConfidenceBadge = 'badge-medium';

if (approvals > 0 && changesRequested === 0) {
reviewScore = 100;
reviewConfidence = 'High';
reviewConfidenceBadge = 'badge-high';
} else if (approvals > 0 && changesRequested > 0) {
reviewScore = 50;
reviewConfidence = 'Medium';
reviewConfidenceBadge = 'badge-medium';
} else if (approvals === 0 && changesRequested > 0) {
reviewScore = 0;
reviewConfidence = 'High';
reviewConfidenceBadge = 'badge-high';
} else {
// No approvals, no changes requested
reviewScore = 0;
reviewConfidence = 'Medium';
reviewConfidenceBadge = 'badge-medium';
}

// Response Score (direct mapping)
const responseScore = responseRate;
let responseConfidence = 'High';
let responseConfidenceBadge = 'badge-high';

if (responseRate < 50) {
responseConfidence = 'Low';
responseConfidenceBadge = 'badge-low';
} else if (responseRate < 80) {
responseConfidence = 'Medium';
responseConfidenceBadge = 'badge-medium';
}

// Calculate Overall Score
// Formula: (CI × 0.4) + (Review × 0.4) + (Response × 0.2) - (3 × conversations)
let overallScore = (ciScore * 0.4) + (reviewScore * 0.4) + (responseScore * 0.2);

// Deduct 3 points per unresolved conversation
overallScore = Math.max(0, overallScore - (conversations * 3));

// Determine overall status
let overallStatus = '';
let overallColor = '';

if (overallScore >= 80) {
overallStatus = '✅ Merge Ready';
overallColor = 'text-green-600 dark:text-green-400';
} else if (overallScore >= 60) {
overallStatus = '⚠️ Needs Attention';
overallColor = 'text-yellow-600 dark:text-yellow-400';
} else {
overallStatus = '❌ Not Ready';
overallColor = 'text-red-600 dark:text-red-400';
}

// Update UI
const overallScoreElement = document.getElementById('overallScore');
overallScoreElement.textContent = Math.round(overallScore) + '%';
overallScoreElement.className = overallColor + ' text-5xl font-bold mb-2';

document.getElementById('overallStatus').textContent = overallStatus;

document.getElementById('ciScoreDisplay').innerHTML =
`${Math.round(ciScore)}% <span class="${ciConfidenceBadge} px-2 py-0.5 rounded text-xs ml-1">${ciConfidence}</span>`;

document.getElementById('reviewScoreDisplay').innerHTML =
`${Math.round(reviewScore)}% <span class="${reviewConfidenceBadge} px-2 py-0.5 rounded text-xs ml-1">${reviewConfidence}</span>`;

document.getElementById('responseScoreDisplay').innerHTML =
`${Math.round(responseScore)}% <span class="${responseConfidenceBadge} px-2 py-0.5 rounded text-xs ml-1">${responseConfidence}</span>`;
}

// Initialize calculator on page load
document.addEventListener('DOMContentLoaded', function () {
// Attach event listeners to all range inputs
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
input.addEventListener('input', calculateScore);
});

// Initial calculation
calculateScore();
});
Loading
Loading