No update data logged to generate statistics.
';
return;
}
const statsMap = {
'Total Updates': stats.count,
'Latest Version': stats.currentVersion,
'Passed Tests': stats.passed,
'Failed Tests': stats.failed,
'Success Rate (%)': stats.successRate + '%'
};
for (const label in statsMap) {
const card = document.createElement('div');
card.className = 'fu-stat-card';
card.innerHTML = `
${statsMap[label]}
${label}
`;
statsOutput.appendChild(card);
}
generatePreview();
};
// --- Summary & Download Functions (Tab 3) ---
const getReportData = () => {
return {
setup: {
project: inputs.project.value,
model: inputs.model.value,
initialVersion: inputs.initialVersion.value,
devTeam: inputs.devTeam.value
},
log: firmwareLog,
stats: calculateStats()
};
};
const generatePreview = () => {
const data = getReportData();
const stats = data.stats;
const tableRowsHTML = data.log.map((log, index) => `
| ${index + 1} |
${escapeHTML(log.newVersion)} |
${escapeHTML(log.date)} |
${escapeHTML(log.changes)} |
${escapeHTML(log.tester)} |
${escapeHTML(log.status)} |
`).join('');
summaryPreview.innerHTML = `
Firmware Log: ${escapeHTML(data.setup.project)}
Model: ${escapeHTML(data.setup.model)} | Team: ${escapeHTML(data.setup.devTeam)}
1. Summary Statistics
${stats ? `
Initial Version: ${escapeHTML(data.setup.initialVersion)}
Latest Version: ${escapeHTML(stats.currentVersion)}
Total Updates: ${stats.count}
Success Rate: ${stats.successRate}%
` : `
No data available for statistics.
`}
2. Full Update History
| # |
Version |
Date |
Changes/Notes |
Tester |
Status |
${tableRowsHTML.length > 0 ? tableRowsHTML : '| No updates logged. |
'}
`;
};
const downloadTxt = () => {
const data = getReportData();
const stats = data.stats;
let content = `FIRMWARE UPDATE LOG: ${data.setup.project.toUpperCase()}\n`;
content += `========================================================\n`;
content += `Model: ${data.setup.model}\nTeam: ${data.setup.devTeam}\n`;
content += `Initial Version: ${data.setup.initialVersion}\n\n`;
if (stats) {
content += "SUMMARY STATISTICS\n";
content += `Total Updates Logged: ${stats.count}\nLatest Version: ${stats.currentVersion}\nSuccess Rate: ${stats.successRate}%\n\n`;
}
content += "UPDATE HISTORY\n";
content += "--------------------------------------------------------\n";
content += " # | Version | Date\t\t| Tester\t| Status\t| Changes/Notes\n";
content += "--------------------------------------------------------\n";
data.log.forEach((log, index) => {
content += `${(index + 1).toString().padStart(2)} | ${log.newVersion.padEnd(7)} | ${log.date}\t| ${log.tester.padEnd(10)}\t| ${log.status.padEnd(7)}\t| ${log.changes}\n`;
});
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${data.setup.project.replace(/ /g, '_')}_firmware_log.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined' || typeof window.jspdf.autoTable === 'undefined') {
alert('Error: jsPDF libraries not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l', 'mm', 'a4'); // Landscape for wide table
const data = getReportData();
const stats = data.stats;
const margin = 10;
const pageWidth = doc.internal.pageSize.getWidth();
let yPos = 15;
// --- PDF Helper ---
const addSectionTitle = (title) => {
doc.setFontSize(14);
doc.setFont(undefined, 'bold');
doc.setTextColor(0, 188, 212); // Teal
doc.text(title, margin, yPos);
doc.setDrawColor(224, 224, 224);
doc.line(margin, yPos + 1, pageWidth - margin, yPos + 1);
yPos += 8;
};
// 1. Header
doc.setFontSize(20);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Firmware Update Log: ${data.setup.project}`, pageWidth / 2, yPos, { align: 'center' });
yPos += 8;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(108, 117, 125);
doc.text(`Model: ${data.setup.model} | Team: ${data.setup.devTeam} | Initial Version: ${data.setup.initialVersion}`, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
// 2. Summary Statistics
if (stats) {
addSectionTitle("1. Summary Statistics");
const statsHead = [['Initial Version', 'Latest Version', 'Total Updates', 'Passed Tests', 'Success Rate (%)']];
const statsBody = [[
data.setup.initialVersion,
stats.currentVersion,
stats.count,
stats.passed,
`${stats.successRate}%`
]];
doc.autoTable({
startY: yPos,
head: statsHead,
body: statsBody,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94], halign: 'center' },
headStyles: { fillColor: [0, 188, 212], textColor: [255, 255, 255] },
margin: { left: margin, right: margin }
});
yPos = doc.autoTable.previous.finalY + 10;
}
// 3. Raw Log Table
addSectionTitle("2. Full Update History");
const logHead = [['#', 'Version', 'Date', 'Changes/Notes', 'Tester', 'Status']];
const logBody = data.log.map((log, index) => [
index + 1,
log.newVersion,
log.date,
log.changes,
log.tester,
log.status
]);
doc.autoTable({
startY: yPos,
head: logHead,
body: logBody,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, textColor: [52, 73, 94], valign: 'top' },
headStyles: { fillColor: [230, 230, 230], textColor: [44, 62, 80] },
columnStyles: { 0: { cellWidth: 8, halign: 'center' }, 3: { cellWidth: 80 } }, // Give notes more space
margin: { left: margin, right: margin }
});
doc.save(`${data.setup.project.replace(/ /g, '_')}_firmware_log.pdf`);
};
// --- Event Listeners ---
// Global Navigation
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2 Actions (Add, Remove, Edit)
newUpdateInputs.addBtn.addEventListener('click', addUpdate);
updatesTbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeUpdate(parseInt(e.target.dataset.removeId));
}
});
updatesTbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const id = parseInt(e.target.dataset.id);
const field = e.target.dataset.field;
updateUpdate(id, field, e.target.textContent);
}
}, true);
// Tab 3 Actions
refreshBtn.addEventListener('click', calculateAndRenderStats);
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
newUpdateInputs.date.value = new Date().toISOString().substring(0, 10);
showTab(1); // Set initial state
});