pulse-monitor/html_report.go

261 lines
9.3 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"os"
)
type ReviewEntry struct {
FrameNum int
Timestamp string
SpO2Value int
SpO2LeftDigit int // tens digit (for compatibility)
SpO2LeftConf float64 // confidence score
SpO2RightDigit int // ones digit (for compatibility)
SpO2RightConf float64 // same as SpO2LeftConf for whole-number matching
HRValue int
HRLeftDigit int
HRLeftConf float64
HRRightDigit int
HRRightConf float64
Failed bool
FailureReason string
Unstable bool
UnstableReason string
}
// initReviewHTML creates the HTML file with header and opens the table
func initReviewHTML() error {
html := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Pulse-Ox Recognition Review</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #000; color: #fff; }
h1 { color: #fff; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #444; padding: 12px; text-align: center; }
th { background-color: #2a5c2e; color: white; }
tr:nth-child(even) { background-color: #1a1a1a; }
tr:nth-child(odd) { background-color: #0a0a0a; }
.conf-high { color: #4CAF50; font-weight: bold; }
.conf-low { color: #f44336; font-weight: bold; }
.crop-img { max-height: 60px; cursor: pointer; transition: transform 0.2s; border: 2px solid #444; }
.crop-img:hover { opacity: 0.8; border-color: #4CAF50; }
.ocr-value { font-size: 42px; font-weight: bold; }
p { color: #ccc; }
strong { color: #fff; }
code { background-color: #222; padding: 2px 6px; border-radius: 3px; color: #4CAF50; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.95); }
.modal-content { margin: auto; display: block; max-width: 90%; max-height: 90%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.close { position: absolute; top: 20px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; cursor: pointer; }
.close:hover { color: #bbb; }
.approve-input { width: 60px; font-size: 18px; padding: 5px; text-align: center; background: #222; color: #fff; border: 2px solid #444; border-radius: 4px; }
.approve-input:focus { border-color: #4CAF50; outline: none; }
.approve-input.success { border-color: #4CAF50; background: #1a3a1a; }
.approve-input.error { border-color: #f44336; background: #3a1a1a; }
.approve-status { font-size: 12px; margin-top: 5px; }
</style>
<script>
function enlargeImage(imgSrc) {
var modal = document.getElementById('imageModal');
var modalImg = document.getElementById('enlargedImg');
modal.style.display = 'block';
modalImg.src = imgSrc;
}
function closeModal() {
document.getElementById('imageModal').style.display = 'none';
}
window.onclick = function(event) {
var modal = document.getElementById('imageModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
function approve(input, frame, display) {
var number = parseInt(input.value);
if (isNaN(number) || number < 0 || number > 200) {
input.className = 'approve-input error';
return;
}
fetch('/approve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({frame: frame, display: display, number: number})
})
.then(response => response.json())
.then(data => {
if (data.status === 'ok') {
input.className = 'approve-input success';
input.disabled = true;
// Hide the image since it's been moved
var img = document.getElementById('img_' + frame + '_' + display);
if (img) img.style.opacity = '0.3';
} else {
input.className = 'approve-input error';
}
})
.catch(err => {
input.className = 'approve-input error';
console.error(err);
});
}
function handleKey(event, input, frame, display) {
if (event.key === 'Enter') {
approve(input, frame, display);
}
}
</script>
</head>
<body>
<div id="imageModal" class="modal" onclick="closeModal()">
<span class="close" onclick="closeModal()">&times;</span>
<img class="modal-content" id="enlargedImg">
</div>
<h1>Pulse-Ox Recognition Review (Live)</h1>
<p><strong>Tip:</strong> Type correct number in input box and press Enter to save as template</p>
<p><em>Refresh page to see latest frames...</em></p>
<table>
<tr>
<th>Frame</th>
<th>Time</th>
<th>SpO2 / HR</th>
</tr>
`
return os.WriteFile("review/review.html", []byte(html), 0644)
}
// appendReviewEntry appends a single entry to the HTML file
func appendReviewEntry(e ReviewEntry) error {
confClass := func(conf float64) string {
if conf >= 95 {
return "conf-high"
}
return "conf-low"
}
// Use left conf as the whole-number confidence
spo2Conf := e.SpO2LeftConf
hrConf := e.HRLeftConf
needsReview := spo2Conf < 95 || hrConf < 95
reviewMarker := ""
if needsReview {
reviewMarker = " ⚠CHECK"
}
var rowHTML string
if e.Failed {
// Failed recognition entry
rowHTML = fmt.Sprintf(` <tr style="background-color: #330000;">
<td>%d%s</td>
<td>%s</td>
<td style="color: #ff6666;">
<strong style="color: #ff6666;">❌ %s</strong><br>
<div style="display: flex; gap: 40px; justify-content: center; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<img id="img_%d_spo2" class="crop-img" src="f%d_spo2_crop.png" onclick="enlargeImage('f%d_spo2_crop.png')">
<div>
<span class="ocr-value">%d</span><br>
<span class="%s">(%.0f%%)</span><br>
<input class="approve-input" type="text" placeholder="#" onkeydown="handleKey(event, this, %d, 'spo2')">
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<img id="img_%d_hr" class="crop-img" src="f%d_hr_crop.png" onclick="enlargeImage('f%d_hr_crop.png')">
<div>
<span class="ocr-value">%d</span><br>
<span class="%s">(%.0f%%)</span><br>
<input class="approve-input" type="text" placeholder="#" onkeydown="handleKey(event, this, %d, 'hr')">
</div>
</div>
</div>
</td>
</tr>
`, e.FrameNum, reviewMarker, e.Timestamp,
e.FailureReason,
e.FrameNum, e.FrameNum, e.FrameNum,
e.SpO2Value, confClass(spo2Conf), spo2Conf, e.FrameNum,
e.FrameNum, e.FrameNum, e.FrameNum,
e.HRValue, confClass(hrConf), hrConf, e.FrameNum)
} else {
// Successful recognition entry
unstableMarker := ""
if e.Unstable {
unstableMarker = fmt.Sprintf(" <br><span style='color: #ff9800; font-size: 12px;'>⚠️ %s</span>", e.UnstableReason)
}
rowHTML = fmt.Sprintf(` <tr>
<td>%d%s%s</td>
<td>%s</td>
<td>
<div style="display: flex; gap: 40px; justify-content: center; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<img id="img_%d_spo2" class="crop-img" src="f%d_spo2_crop.png" onclick="enlargeImage('f%d_spo2_crop.png')">
<div>
<span class="ocr-value">%d</span><br>
<span class="%s">(%.0f%%)</span><br>
<input class="approve-input" type="text" placeholder="#" onkeydown="handleKey(event, this, %d, 'spo2')">
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<img id="img_%d_hr" class="crop-img" src="f%d_hr_crop.png" onclick="enlargeImage('f%d_hr_crop.png')">
<div>
<span class="ocr-value">%d</span><br>
<span class="%s">(%.0f%%)</span><br>
<input class="approve-input" type="text" placeholder="#" onkeydown="handleKey(event, this, %d, 'hr')">
</div>
</div>
</div>
</td>
</tr>
`, e.FrameNum, reviewMarker, unstableMarker, e.Timestamp,
e.FrameNum, e.FrameNum, e.FrameNum,
e.SpO2Value, confClass(spo2Conf), spo2Conf, e.FrameNum,
e.FrameNum, e.FrameNum, e.FrameNum,
e.HRValue, confClass(hrConf), hrConf, e.FrameNum)
}
// Open file in append mode
f, err := os.OpenFile("review/review.html", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(rowHTML)
return err
}
// closeReviewHTML writes the closing tags
func closeReviewHTML() error {
footer := ` </table>
<p style="margin-top: 20px;">
<strong>Color coding:</strong>
<span class="conf-high">Green ≥95%</span>,
<span class="conf-low">Red &lt;95%</span>
</p>
</body>
</html>`
f, err := os.OpenFile("review/review.html", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(footer)
return err
}
// writeReviewHTML - kept for backward compatibility (final shutdown)
func writeReviewHTML(entries []ReviewEntry) error {
// Just close the HTML properly - entries already appended
return closeReviewHTML()
}