feat: monitoring session improvements — UTC fix, period hours, calendar, session detail
- Fix UTC display bug: upload_nrl_data now wraps RNH datetimes with
local_to_utc() before storing, matching patch_session behavior.
Period type and label are derived from local time before conversion.
- Add period_start_hour / period_end_hour to MonitoringSession model
(nullable integers 0–23). Migration: migrate_add_session_period_hours.py
- Update patch_session to accept and store period_start_hour / period_end_hour.
Response now includes both fields.
- Update get_project_sessions to compute "Effective: M/D H:MM AM → M/D H:MM AM"
string from period hours and pass it to session_list.html.
- Rework period edit UI in session_list.html: clicking the period badge now
opens an inline editor with period type selector + start/end hour inputs.
Selecting a period type pre-fills default hours (Day: 7–19, Night: 19–7).
- Wire period hours into _build_location_data_from_sessions: uses
period_start/end_hour when set, falls back to hardcoded defaults.
- RND viewer: inject SESSION_PERIOD_START/END_HOUR from template context.
renderTable() dims rows outside the period window (opacity-40) with a
tooltip; shows "(N in period window)" in the row count.
- New session detail page at /api/projects/{id}/sessions/{id}/detail:
shows breadcrumb, files list with View/Download/Report actions,
editable session info form (label, period type, hours, times).
- Add local_datetime_input Jinja filter for datetime-local input values.
- Monthly calendar view: new get_sessions_calendar endpoint returns
sessions_calendar.html partial; added below sessions list in detail.html.
Color-coded per NRL with legend, HTMX prev/next navigation, session dots
link to detail page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,27 @@ let chartInstance = null;
|
||||
let allData = [];
|
||||
let allHeaders = [];
|
||||
|
||||
// Session period window (null = no filtering)
|
||||
const SESSION_PERIOD_START_HOUR = {{ period_start_hour if period_start_hour is not none else 'null' }};
|
||||
const SESSION_PERIOD_END_HOUR = {{ period_end_hour if period_end_hour is not none else 'null' }};
|
||||
|
||||
/**
|
||||
* Returns true if the given hour integer is within the session's period window.
|
||||
* Always returns true when no period window is configured.
|
||||
*/
|
||||
function _isInPeriodWindow(hour) {
|
||||
if (SESSION_PERIOD_START_HOUR === null || SESSION_PERIOD_END_HOUR === null) return true;
|
||||
const sh = SESSION_PERIOD_START_HOUR;
|
||||
const eh = SESSION_PERIOD_END_HOUR;
|
||||
if (eh > sh) {
|
||||
// Same-day window, e.g. 7–19
|
||||
return hour >= sh && hour < eh;
|
||||
} else {
|
||||
// Crosses midnight, e.g. 19–7
|
||||
return hour >= sh || hour < eh;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadRndData();
|
||||
@@ -387,19 +408,21 @@ function renderChart(data, fileType) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(headers, data) {
|
||||
const headerRow = document.getElementById('table-header');
|
||||
const tbody = document.getElementById('table-body');
|
||||
function _rowHour(row) {
|
||||
// Parse hour from "Start Time" field (format: "YYYY/MM/DD HH:MM:SS")
|
||||
const t = row['Start Time'];
|
||||
if (!t) return null;
|
||||
const parts = t.split(' ');
|
||||
if (parts.length < 2) return null;
|
||||
return parseInt(parts[1].split(':')[0], 10);
|
||||
}
|
||||
|
||||
// Render headers
|
||||
headerRow.innerHTML = '<tr>' + headers.map(h =>
|
||||
`<th class="px-4 py-3 text-left font-medium">${escapeHtml(h)}</th>`
|
||||
).join('') + '</tr>';
|
||||
|
||||
// Render rows (limit to first 500 for performance)
|
||||
const displayData = data.slice(0, 500);
|
||||
tbody.innerHTML = displayData.map(row =>
|
||||
'<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">' +
|
||||
function _buildRow(headers, row) {
|
||||
const hour = _rowHour(row);
|
||||
const inWindow = hour === null || _isInPeriodWindow(hour);
|
||||
const dimClass = inWindow ? '' : 'opacity-40';
|
||||
const titleAttr = (!inWindow) ? ' title="Outside period window"' : '';
|
||||
return `<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 ${dimClass}"${titleAttr}>` +
|
||||
headers.map(h => {
|
||||
const val = row[h];
|
||||
let displayVal = val;
|
||||
@@ -410,12 +433,34 @@ function renderTable(headers, data) {
|
||||
}
|
||||
return `<td class="px-4 py-2 text-gray-700 dark:text-gray-300">${escapeHtml(String(displayVal))}</td>`;
|
||||
}).join('') +
|
||||
'</tr>'
|
||||
).join('');
|
||||
'</tr>';
|
||||
}
|
||||
|
||||
function renderTable(headers, data) {
|
||||
const headerRow = document.getElementById('table-header');
|
||||
const tbody = document.getElementById('table-body');
|
||||
|
||||
// Render headers — add period window indicator if configured
|
||||
let periodNote = '';
|
||||
if (SESSION_PERIOD_START_HOUR !== null && SESSION_PERIOD_END_HOUR !== null) {
|
||||
function _fmtH(h) { const ampm = h < 12 ? 'AM' : 'PM'; return `${h%12||12}:00 ${ampm}`; }
|
||||
periodNote = ` <span class="ml-2 text-indigo-500 dark:text-indigo-400 font-normal normal-case text-xs" title="Dimmed rows are outside this window">Period: ${_fmtH(SESSION_PERIOD_START_HOUR)} → ${_fmtH(SESSION_PERIOD_END_HOUR)}</span>`;
|
||||
}
|
||||
headerRow.innerHTML = '<tr>' + headers.map((h, i) =>
|
||||
`<th class="px-4 py-3 text-left font-medium">${escapeHtml(h)}${i === 0 ? periodNote : ''}</th>`
|
||||
).join('') + '</tr>';
|
||||
|
||||
// Render rows (limit to first 500 for performance)
|
||||
const displayData = data.slice(0, 500);
|
||||
tbody.innerHTML = displayData.map(row => _buildRow(headers, row)).join('');
|
||||
|
||||
// Update row count
|
||||
const inWindowCount = data.filter(r => { const h = _rowHour(r); return h === null || _isInPeriodWindow(h); }).length;
|
||||
const windowNote = (SESSION_PERIOD_START_HOUR !== null && inWindowCount < data.length)
|
||||
? ` (${inWindowCount} in period window)`
|
||||
: '';
|
||||
document.getElementById('row-count').textContent =
|
||||
data.length > 500 ? `Showing 500 of ${data.length.toLocaleString()} rows` : `${data.length.toLocaleString()} rows`;
|
||||
data.length > 500 ? `Showing 500 of ${data.length.toLocaleString()} rows${windowNote}` : `${data.length.toLocaleString()} rows${windowNote}`;
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('table-search').addEventListener('input', function(e) {
|
||||
@@ -427,20 +472,7 @@ function renderTable(headers, data) {
|
||||
);
|
||||
|
||||
const displayFiltered = filtered.slice(0, 500);
|
||||
tbody.innerHTML = displayFiltered.map(row =>
|
||||
'<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">' +
|
||||
headers.map(h => {
|
||||
const val = row[h];
|
||||
let displayVal = val;
|
||||
if (val === null || val === undefined) {
|
||||
displayVal = '-';
|
||||
} else if (typeof val === 'number') {
|
||||
displayVal = val.toFixed(1);
|
||||
}
|
||||
return `<td class="px-4 py-2 text-gray-700 dark:text-gray-300">${escapeHtml(String(displayVal))}</td>`;
|
||||
}).join('') +
|
||||
'</tr>'
|
||||
).join('');
|
||||
tbody.innerHTML = displayFiltered.map(row => _buildRow(headers, row)).join('');
|
||||
|
||||
document.getElementById('row-count').textContent =
|
||||
filtered.length > 500 ? `Showing 500 of ${filtered.length.toLocaleString()} filtered rows` : `${filtered.length.toLocaleString()} rows`;
|
||||
|
||||
Reference in New Issue
Block a user