v0.20.0 -- Full s3 event parse and PDF creation. #28

Merged
serversdown merged 46 commits from dev into main 2026-05-28 17:54:34 -04:00
Showing only changes of commit c14a8c54db - Show all commits
+193 -28
View File
@@ -161,7 +161,7 @@
background: #161b22; background: #161b22;
border: 1px solid #21262d; border: 1px solid #21262d;
border-radius: 8px; border-radius: 8px;
padding: 10px 12px 8px; padding: 10px 30px 8px 12px; /* right padding leaves room for the "0.0" baseline label */
} }
.chart-label { .chart-label {
font-size: 11px; font-size: 11px;
@@ -211,6 +211,72 @@
font-size: 11px; font-size: 11px;
margin-left: 8px; margin-left: 8px;
} }
/* Per-channel stats table in the metadata header */
.stats-table {
grid-column: 1 / -1;
border-collapse: collapse;
font-family: monospace;
font-size: 12px;
margin-top: 4px;
}
.stats-table th, .stats-table td {
padding: 3px 14px 3px 0;
text-align: left;
color: #c9d1d9;
}
.stats-table th {
color: #484f58;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
/* ── Print view (light theme matching the Instantel printout) ─── */
body.print-view {
background: #ffffff;
color: #000000;
}
body.print-view header,
body.print-view #event-list-wrap,
body.print-view #event-list-header,
body.print-view #event-meta,
body.print-view #status-bar,
body.print-view .chart-wrap {
background: #ffffff;
border-color: #cccccc;
color: #000000;
}
body.print-view .event-row { color: #000; border-bottom-color: #eee; }
body.print-view .event-row:hover { background: #f4f4f4; }
body.print-view .event-row.active {
background: #e6f0ff;
border-left-color: #1f6feb;
}
body.print-view .er-ts { color: #000; }
body.print-view .er-pvs { color: #003a8c; }
body.print-view .er-meta,
body.print-view #event-list-header,
body.print-view .meta-field .mf-label,
body.print-view .stats-table th {
color: #666;
}
body.print-view .mf-value { color: #000; }
body.print-view .mf-value.highlight { color: #003a8c; }
body.print-view label { color: #444; }
body.print-view input, body.print-view select {
background: #fff; color: #000; border-color: #ccc;
}
/* In print theme, the channel-label colors stay (they identify
the trace). Only the chart panel background flips. */
@media print {
header, #event-list-wrap, #status-bar, button { display: none !important; }
body { overflow: visible; height: auto; }
#main, #viewer { overflow: visible; }
#charts { overflow: visible; }
}
</style> </style>
</head> </head>
<body> <body>
@@ -223,7 +289,8 @@
</select> </select>
<input type="search" id="event-filter" placeholder="filter events…" /> <input type="search" id="event-filter" placeholder="filter events…" />
<span class="pill" id="count-pill"></span> <span class="pill" id="count-pill"></span>
<button id="reload-btn" onclick="loadSerials()" style="margin-left:auto">Reload</button> <button id="print-btn" onclick="togglePrintView()" style="margin-left:auto;background:#21262d">Print view</button>
<button id="reload-btn" onclick="loadSerials()">Reload</button>
</header> </header>
<div id="main"> <div id="main">
@@ -250,13 +317,16 @@
<div id="status-bar">Ready.</div> <div id="status-bar">Ready.</div>
<script> <script>
// Channel colors and rendering order mirror Instantel's BW Event Report
// printout: MicL at the top, Tran at the bottom. Colors approximate
// what BW renders (magenta mic, blue long, green vert, red tran).
const CHANNEL_COLORS = { const CHANNEL_COLORS = {
Tran: '#58a6ff', MicL: '#e066ff',
Long: '#3a80ff',
Vert: '#3fb950', Vert: '#3fb950',
Long: '#d29922', Tran: '#f85149',
MicL: '#bc8cff',
}; };
const CHANNEL_ORDER = ['Tran', 'Vert', 'Long', 'MicL']; const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
let allEvents = []; let allEvents = [];
let filteredEvents = []; let filteredEvents = [];
@@ -401,14 +471,48 @@ function renderMeta(data, ev) {
['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'], ['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'],
['Project', ev?.project || '—'], ['Project', ev?.project || '—'],
['Location', ev?.sensor_location || '—'], ['Location', ev?.sensor_location || '—'],
['PVS', ev?.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(4)} in/s` : '—'], ['Peak Vector Sum',
ev?.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(4)} in/s` : '—'],
]; ];
metaDiv.innerHTML = fields.map(([l, v]) =>
`<div class="meta-field"><span class="mf-label">${l}</span><span class="mf-value${l === 'PVS' ? ' highlight' : ''}">${v}</span></div>` // Per-channel stats table mirroring the printout's middle block.
).join(''); // Pulls per-channel PPV from the events row (DB columns) and additional
// details (peak time, peak accel, peak displacement, sensor check) from
// bw_report when present.
const fmt = v => (v == null ? '—' : (typeof v === 'number' ? v.toFixed(3) : v));
const rows = [
['Tran', ev?.tran_ppv],
['Vert', ev?.vert_ppv],
['Long', ev?.long_ppv],
];
const statsHtml = `
<table class="stats-table">
<thead>
<tr><th>Channel</th><th>PPV (in/s)</th></tr>
</thead>
<tbody>
${rows.map(([ch, ppv]) => `<tr><td>${ch}</td><td>${fmt(ppv)}</td></tr>`).join('')}
<tr><td>MicL</td><td>${fmt(ev?.mic_ppv)} psi</td></tr>
</tbody>
</table>
`;
metaDiv.innerHTML =
fields.map(([l, v]) =>
`<div class="meta-field"><span class="mf-label">${l}</span><span class="mf-value${l === 'Peak Vector Sum' ? ' highlight' : ''}">${v}</span></div>`
).join('') + statsHtml;
metaDiv.style.display = 'grid'; metaDiv.style.display = 'grid';
} }
function togglePrintView() {
document.body.classList.toggle('print-view');
// Force chart redraw so axis/grid colors are re-evaluated against the
// new background. Easiest: re-render the current event.
if (currentEventId) {
loadEvent(currentEventId);
}
}
function renderWaveform(data) { function renderWaveform(data) {
document.getElementById('empty-state').style.display = 'none'; document.getElementById('empty-state').style.display = 'none';
const chartsDiv = document.getElementById('charts'); const chartsDiv = document.getElementById('charts');
@@ -420,6 +524,15 @@ function renderWaveform(data) {
const channels = data.channels || {}; const channels = data.channels || {};
const timeAxis = data.time_axis || null; // ms relative to trigger const timeAxis = data.time_axis || null; // ms relative to trigger
const triggerMs = data.trigger_ms ?? 0; const triggerMs = data.trigger_ms ?? 0;
const isPrintMode = document.body.classList.contains('print-view');
// Which channels actually have data → determines which one renders the
// shared x-axis at the bottom (Instantel printout has the time scale
// only on the bottom-most chart).
const channelsWithData = CHANNEL_ORDER.filter(ch =>
channels[ch] && (channels[ch].values || []).length > 0
);
const lastDataCh = channelsWithData[channelsWithData.length - 1];
for (const ch of CHANNEL_ORDER) { for (const ch of CHANNEL_ORDER) {
const chData = channels[ch]; const chData = channels[ch];
@@ -447,6 +560,9 @@ function renderWaveform(data) {
? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}` ? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}`
+ (peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '') + (peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
: ''; : '';
// Hide x-axis on every chart except the bottom-most data channel —
// gives the "single shared time axis" feel of the BW printout.
const showXAxis = (ch === lastDataCh);
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = 'chart-wrap'; wrap.className = 'chart-wrap';
@@ -510,40 +626,89 @@ function renderWaveform(data) {
scales: { scales: {
x: { x: {
type: 'category', type: 'category',
display: showXAxis,
ticks: { ticks: {
color: '#484f58', color: isPrintMode ? '#666' : '#484f58',
maxTicksLimit: 10, maxTicksLimit: 10,
maxRotation: 0, maxRotation: 0,
callback: (val, i) => rT[i] + ' ms', callback: (val, i) => rT[i] + ' ms',
}, },
grid: { color: '#21262d' }, grid: { color: isPrintMode ? '#e0e0e0' : '#21262d', drawTicks: showXAxis },
}, },
y: { y: {
ticks: { color: '#484f58', maxTicksLimit: 5 }, ticks: { color: isPrintMode ? '#666' : '#484f58', maxTicksLimit: 5 },
grid: { color: '#21262d' }, grid: { color: isPrintMode ? '#e0e0e0' : '#21262d' },
title: { display: true, text: unit, color: '#484f58', font: { size: 10 } }, title: { display: true, text: unit,
color: isPrintMode ? '#666' : '#484f58', font: { size: 10 } },
}, },
}, },
}, },
plugins: [{ plugins: [{
// Vertical trigger line at t=0 // Trigger line @ t=0 + triangle markers above/below + "0.0"
id: 'triggerLine', // baseline label on the right edge. Matches the Instantel
// BW Event Report printout style.
id: 'instantelOverlays',
afterDraw(chart) { afterDraw(chart) {
const ctx = chart.ctx; const ctx = chart.ctx;
const xAxis = chart.scales.x; const xAxis = chart.scales.x;
const yAxis = chart.scales.y; const yAxis = chart.scales.y;
const fgPrim = isPrintMode ? '#000' : '#c9d1d9';
const fgTrigger = '#f85149';
// Dashed vertical trigger line at t=0
const zeroIdx = rT.findIndex(t => parseFloat(t) >= 0); const zeroIdx = rT.findIndex(t => parseFloat(t) >= 0);
if (zeroIdx < 0) return; if (zeroIdx >= 0) {
const x = xAxis.getPixelForValue(zeroIdx); const x = xAxis.getPixelForValue(zeroIdx);
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, yAxis.top); ctx.moveTo(x, yAxis.top);
ctx.lineTo(x, yAxis.bottom); ctx.lineTo(x, yAxis.bottom);
ctx.strokeStyle = 'rgba(248, 81, 73, 0.7)'; ctx.strokeStyle = isPrintMode ? '#cc0000' : 'rgba(248, 81, 73, 0.8)';
ctx.lineWidth = 1.5; ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]); ctx.setLineDash([4, 3]);
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
// Triangles above and below the chart at the trigger column
ctx.save();
ctx.fillStyle = fgTrigger;
ctx.beginPath(); // top triangle pointing down
ctx.moveTo(x - 5, yAxis.top - 8);
ctx.lineTo(x + 5, yAxis.top - 8);
ctx.lineTo(x, yAxis.top - 1);
ctx.closePath();
ctx.fill();
ctx.beginPath(); // bottom triangle pointing up
ctx.moveTo(x - 5, yAxis.bottom + 8);
ctx.lineTo(x + 5, yAxis.bottom + 8);
ctx.lineTo(x, yAxis.bottom + 1);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// "0.0" baseline label on the right edge — printout convention.
// Position vertically at the zero-amplitude level.
const zeroY = yAxis.getPixelForValue(0);
if (zeroY >= yAxis.top && zeroY <= yAxis.bottom) {
ctx.save();
ctx.strokeStyle = isPrintMode ? '#aaa' : '#30363d';
ctx.lineWidth = 0.8;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(xAxis.left, zeroY);
ctx.lineTo(xAxis.right, zeroY);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.fillStyle = fgPrim;
ctx.font = '11px monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('0.0', xAxis.right + 6, zeroY);
ctx.restore();
}
}, },
}], }],
}); });