Skip to content

Commit f234bb6

Browse files
committed
test(visual): add capture-time convention coverage and standardize header placement
- add pytest coverage for Capture Time header convention in CA, template, and updated visuals - standardize Capture Time as a header-row pill next to the visual title (layout-only) - format pnm_header.capture_time as human-readable UTC date/time (not raw epoch seconds) - update Histogram visual to show Capture Time in header and remove measurement stats panel - update Pre-Equalization, RxMER, and Constellation visuals to support optional Capture Time header display - update visual template scaffold with reusable Capture Time header pattern and formatter helper - 2026-02-22 22:36:38
1 parent c877d41 commit f234bb6

15 files changed

Lines changed: 539 additions & 326 deletions

CODING_AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
- Prefer a horizontal one-row table for common modem identity fields with display labels exactly:
4343
- `MacAddress`, `Model`, `Vendor`, `SW Version`, `HW Version`, `Boot ROM`
4444
- Use proper display casing/spacing for labels (for example `SW Version`, not `SW_REV`; `Boot ROM`, not `BOOTR`).
45+
- When a capture timestamp is available (for example `pnm_header.capture_time`), place `Capture Time` next to the visual title/header as a layout-only element (not inside chart/graph sections).
46+
- Format capture timestamps as a human-readable date/time string (not raw epoch seconds).
4547
- Keep channel metadata in a separate block below `Device Info` (for example `Channel`, center/subcarrier frequency).
4648
- Display frequencies in `MHz` for UI-facing labels; raw `Hz` may be shown secondarily in parentheses when useful.
4749
- Avoid redundant repetition of values already shown in `Device Info` (for example, do not repeat `MacAddress` in the channel header if it is already in the device table).

docs/visual-previews/SingleCapture/Histogram/Histogram.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/visual-previews/SingleCapture/OFDM/GetCapture-ConstellationDisplay.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/visual-previews/SingleCapture/OFDM/GetCapture-RxMER.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/visual-previews/SingleCapture/OFDMA/GetCapture-PreEqualization.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/visual/SingleCapture/Histogram/Histogram.md

Lines changed: 149 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -15,103 +15,104 @@ Preview is best-effort. Some templates may rely on Postman-specific APIs that ar
1515
<summary>Visualizer HTML/script source</summary>
1616

1717
````html
18-
// Visualization Script
18+
// Remarks (CODING_AGENTS visual standard)
19+
// - Device Info uses a separate top block with standardized labels
20+
// - Histogram chart remains the primary visualization
21+
// - Missing sysDescr fields render as N/A
1922
const template = `
2023
<style>
2124
body {
2225
font-family: Arial, sans-serif;
23-
padding: 20px;
24-
background-color: #1e1e1e;
25-
color: #e0e0e0;
26+
padding: 16px;
27+
background-color: #0b0b0b;
28+
color: #e8e8e8;
2629
}
2730
.container {
28-
max-width: 1200px;
31+
max-width: 1280px;
2932
margin: 0 auto;
3033
}
3134
.header {
32-
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
33-
color: #e0e0e0;
34-
padding: 20px;
35-
border-radius: 8px;
36-
margin-bottom: 20px;
37-
}
38-
.header h1 {
39-
margin: 0 0 10px 0;
40-
font-size: 24px;
41-
color: #e0e0e0;
42-
}
43-
.header p {
44-
margin: 5px 0;
45-
opacity: 0.9;
46-
color: #b0b0b0;
47-
}
48-
.device-info {
49-
background: #2d2d2d;
50-
padding: 20px;
51-
border-radius: 8px;
52-
margin-bottom: 20px;
53-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
54-
}
55-
.device-info h2 {
56-
margin-top: 0;
57-
color: #e0e0e0;
58-
border-bottom: 2px solid #4a5568;
59-
padding-bottom: 10px;
60-
}
61-
.info-grid {
35+
margin-bottom: 12px;
6236
display: grid;
63-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
64-
gap: 15px;
65-
margin-top: 15px;
37+
grid-template-columns: 1fr auto 1fr;
38+
align-items: center;
39+
gap: 8px;
6640
}
67-
.info-item {
68-
padding: 10px;
69-
background: #3d3d3d;
70-
border-radius: 4px;
41+
.header h1 {
42+
margin: 0;
43+
font-size: 22px;
44+
color: #f2f2f2;
45+
grid-column: 2;
46+
text-align: center;
7147
}
72-
.info-label {
73-
font-weight: bold;
74-
color: #7c9cbf;
48+
.capture-time {
49+
grid-column: 3;
50+
justify-self: end;
7551
font-size: 12px;
76-
text-transform: uppercase;
52+
color: #d7deec;
53+
background: rgba(255,255,255,0.03);
54+
border: 1px solid rgba(255,255,255,0.08);
55+
border-radius: 999px;
56+
padding: 6px 10px;
57+
white-space: nowrap;
7758
}
78-
.info-value {
79-
color: #e0e0e0;
80-
font-size: 16px;
81-
margin-top: 5px;
59+
.panel {
60+
background: #151515;
61+
border: 1px solid #2a2a2a;
62+
border-radius: 10px;
63+
padding: 14px;
64+
margin-bottom: 12px;
8265
}
83-
.chart-container {
84-
background: #2d2d2d;
85-
padding: 20px;
86-
border-radius: 8px;
87-
margin-bottom: 20px;
88-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
66+
.panel-title {
67+
margin: 0 0 10px 0;
68+
font-size: 11px;
69+
text-transform: uppercase;
70+
letter-spacing: 0.7px;
71+
color: #dbe3ff;
8972
}
90-
.chart-container h2 {
91-
margin-top: 0;
92-
color: #e0e0e0;
93-
border-bottom: 2px solid #4a5568;
94-
padding-bottom: 10px;
73+
.table-wrap {
74+
overflow-x: auto;
75+
border: 1px solid rgba(255,255,255,0.08);
76+
border-radius: 10px;
9577
}
96-
.stats-table {
78+
.table {
9779
width: 100%;
80+
min-width: 720px;
9881
border-collapse: collapse;
99-
margin-top: 10px;
10082
}
101-
.stats-table th {
102-
background: #3d3d3d;
103-
color: #e0e0e0;
104-
padding: 10px;
83+
.table th {
10584
text-align: left;
106-
border-bottom: 2px solid #4a5568;
85+
white-space: nowrap;
86+
padding: 9px 12px;
87+
font-size: 11px;
88+
text-transform: uppercase;
89+
letter-spacing: 0.45px;
90+
color: #dbe3ff;
91+
background: rgba(255,255,255,0.03);
92+
}
93+
.table td {
94+
padding: 10px 12px;
95+
font-size: 12px;
96+
color: #ffffff;
97+
white-space: nowrap;
98+
border-top: 1px solid rgba(255,255,255,0.08);
10799
}
108-
.stats-table td {
109-
padding: 8px 10px;
110-
border-bottom: 1px solid #3d3d3d;
111-
color: #b0b0b0;
100+
.mono {
101+
font-family: Consolas, "Liberation Mono", Menlo, monospace;
112102
}
113-
.stats-table tr:hover {
114-
background: #3d3d3d;
103+
.meta-row {
104+
display: flex;
105+
gap: 10px;
106+
flex-wrap: wrap;
107+
margin-top: 8px;
108+
color: #cfd6e5;
109+
font-size: 12px;
110+
}
111+
.meta-pill {
112+
background: rgba(255,255,255,0.03);
113+
border: 1px solid rgba(255,255,255,0.08);
114+
border-radius: 999px;
115+
padding: 6px 10px;
115116
}
116117
canvas {
117118
max-width: 100%;
@@ -121,59 +122,43 @@ const template = `
121122

122123
<div class="container">
123124
<div class="header">
124-
<h1>📊 Histogram Capture Analysis</h1>
125-
<p><strong>MAC Address:</strong> {{mac_address}}</p>
126-
<p><strong>Status:</strong> {{status_message}}</p>
125+
<h1>Histogram Capture Analysis</h1>
126+
<div class="capture-time">Capture Time: {{capture_time}}</div>
127127
</div>
128128

129-
{{#if device_details}}
130-
<div class="device-info">
131-
<h2>🖥️ Device Information</h2>
132-
<div class="info-grid">
133-
<div class="info-item">
134-
<div class="info-label">Vendor</div>
135-
<div class="info-value">{{device_details.VENDOR}}</div>
136-
</div>
137-
<div class="info-item">
138-
<div class="info-label">Model</div>
139-
<div class="info-value">{{device_details.MODEL}}</div>
140-
</div>
141-
<div class="info-item">
142-
<div class="info-label">Hardware Rev</div>
143-
<div class="info-value">{{device_details.HW_REV}}</div>
144-
</div>
145-
<div class="info-item">
146-
<div class="info-label">Software Rev</div>
147-
<div class="info-value">{{device_details.SW_REV}}</div>
148-
</div>
129+
<div class="panel">
130+
<div class="panel-title">Device Info</div>
131+
<div class="table-wrap">
132+
<table class="table">
133+
<thead>
134+
<tr>
135+
<th>MacAddress</th>
136+
<th>Model</th>
137+
<th>Vendor</th>
138+
<th>SW Version</th>
139+
<th>HW Version</th>
140+
<th>Boot ROM</th>
141+
</tr>
142+
</thead>
143+
<tbody>
144+
<tr>
145+
<td class="mono">{{device_info.macAddress}}</td>
146+
<td>{{device_info.MODEL}}</td>
147+
<td>{{device_info.VENDOR}}</td>
148+
<td class="mono">{{device_info.SW_REV}}</td>
149+
<td class="mono">{{device_info.HW_REV}}</td>
150+
<td class="mono">{{device_info.BOOTR}}</td>
151+
</tr>
152+
</tbody>
153+
</table>
154+
</div>
155+
<div class="meta-row">
156+
<div class="meta-pill">Bins: {{bin_count}}</div>
149157
</div>
150158
</div>
151-
{{/if}}
152-
153-
{{#if measurement_stats}}
154-
<div class="device-info">
155-
<h2>📈 Measurement Statistics</h2>
156-
<table class="stats-table">
157-
<thead>
158-
<tr>
159-
<th>Parameter</th>
160-
<th>Value</th>
161-
</tr>
162-
</thead>
163-
<tbody>
164-
{{#each measurement_stats}}
165-
<tr>
166-
<td>{{this.label}}</td>
167-
<td>{{this.value}}</td>
168-
</tr>
169-
{{/each}}
170-
</tbody>
171-
</table>
172-
</div>
173-
{{/if}}
174159

175-
<div class="chart-container">
176-
<h2>📊 Histogram Distribution</h2>
160+
<div class="panel">
161+
<div class="panel-title">Histogram Distribution</div>
177162
<canvas id="histogramChart"></canvas>
178163
</div>
179164
</div>
@@ -195,8 +180,8 @@ const template = `
195180
datasets: [{
196181
label: 'Hit Count',
197182
data: data.chart_data,
198-
backgroundColor: 'rgba(75, 192, 192, 0.6)',
199-
borderColor: 'rgba(75, 192, 192, 1)',
183+
backgroundColor: 'rgba(0, 194, 255, 0.45)',
184+
borderColor: 'rgba(0, 194, 255, 0.95)',
200185
borderWidth: 1
201186
}]
202187
},
@@ -210,7 +195,7 @@ const template = `
210195
fontColor: '#e0e0e0'
211196
},
212197
gridLines: {
213-
color: '#3d3d3d'
198+
color: 'rgba(255,255,255,0.08)'
214199
},
215200
scaleLabel: {
216201
display: true,
@@ -223,7 +208,7 @@ const template = `
223208
fontColor: '#e0e0e0'
224209
},
225210
gridLines: {
226-
color: '#3d3d3d'
211+
color: 'rgba(255,255,255,0.06)'
227212
},
228213
scaleLabel: {
229214
display: true,
@@ -245,39 +230,50 @@ const template = `
245230

246231
function constructVisualizerPayload() {
247232
const response = pm.response.json();
233+
const data = response.data || {};
234+
const firstAnalysis = (data.analysis && data.analysis[0]) || {};
235+
const pnmHeader = firstAnalysis.pnm_header || {};
236+
237+
function formatCaptureTime(raw) {
238+
if (raw === undefined || raw === null || raw === '') return 'N/A';
239+
if (typeof raw === 'number' && isFinite(raw)) {
240+
const ms = raw > 1e12 ? raw : raw * 1000;
241+
const d = new Date(ms);
242+
if (isNaN(d.getTime())) return 'N/A';
243+
return d.toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
244+
}
245+
const n = Number(raw);
246+
if (!isNaN(n) && isFinite(n)) {
247+
return formatCaptureTime(n);
248+
}
249+
const d = new Date(raw);
250+
if (isNaN(d.getTime())) return String(raw);
251+
return d.toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
252+
}
248253

249254
// Extract basic info
250-
const macAddress = response.mac_address || 'N/A';
255+
const macAddress = firstAnalysis.mac_address || response.mac_address || 'N/A';
251256
const status = response.status;
252-
const statusMessage = status === 0 ? '✅ Success' : '❌ Failed';
257+
const statusMessage = status === 0 ? 'Success' : 'Failed';
258+
const captureTime = formatCaptureTime(pnmHeader.capture_time);
253259

254260
// Extract device details
255-
let deviceDetails = null;
256-
if (response.data && response.data.analysis && response.data.analysis.length > 0) {
257-
const analysis = response.data.analysis[0];
258-
if (analysis.device_details && analysis.device_details.system_description) {
259-
deviceDetails = analysis.device_details.system_description;
260-
}
261-
}
262-
263-
// Extract measurement stats
264-
let measurementStats = [];
265-
if (response.data && response.data.measurement_stats && response.data.measurement_stats.length > 0) {
266-
const stats = response.data.measurement_stats[0].entry;
267-
measurementStats = [
268-
{ label: 'Histogram Enabled', value: stats.docsPnmCmDsHistEnable ? 'Yes' : 'No' },
269-
{ label: 'Timeout (seconds)', value: stats.docsPnmCmDsHistTimeOut },
270-
{ label: 'Measurement Status', value: stats.docsPnmCmDsHistMeasStatus },
271-
{ label: 'File Name', value: stats.docsPnmCmDsHistFileName }
272-
];
273-
}
261+
const sys = ((firstAnalysis.device_details || {}).system_description) || {};
262+
const deviceInfo = {
263+
macAddress: macAddress,
264+
MODEL: (sys.MODEL && String(sys.MODEL).trim()) || 'N/A',
265+
VENDOR: (sys.VENDOR && String(sys.VENDOR).trim()) || 'N/A',
266+
SW_REV: (sys.SW_REV && String(sys.SW_REV).trim()) || 'N/A',
267+
HW_REV: (sys.HW_REV && String(sys.HW_REV).trim()) || 'N/A',
268+
BOOTR: (sys.BOOTR && String(sys.BOOTR).trim()) || 'N/A'
269+
};
274270

275271
// Extract histogram data for chart - MODIFIED TO REMOVE "Bin_" PREFIX
276272
let chartLabels = [];
277273
let chartData = [];
278274

279-
if (response.data && response.data.analysis && response.data.analysis.length > 0) {
280-
const analysis = response.data.analysis[0];
275+
if (data.analysis && data.analysis.length > 0) {
276+
const analysis = data.analysis[0];
281277
if (analysis.hit_counts && Array.isArray(analysis.hit_counts)) {
282278
chartData = analysis.hit_counts;
283279
// Generate labels as just the numeric index (0, 1, 2, 3, ...)
@@ -288,10 +284,11 @@ function constructVisualizerPayload() {
288284
return {
289285
mac_address: macAddress,
290286
status_message: statusMessage,
291-
device_details: deviceDetails,
292-
measurement_stats: measurementStats,
287+
capture_time: captureTime,
288+
device_info: deviceInfo,
293289
chart_labels: chartLabels,
294-
chart_data: chartData
290+
chart_data: chartData,
291+
bin_count: chartData.length
295292
};
296293
}
297294

0 commit comments

Comments
 (0)