feat: add 24-Hour (full-day) session period type

Sessions could only be tagged day or night (weekday/weekend). 24/7 continuous
jobs had no fitting period type. Add "24-Hour" (full_24h) — a single full-day
period covering day + night.

UI (session_list.html):
- Full-width "24-Hour" button under the WD/WE x Day/Night grid; teal badge.
- Selecting it clears + disables the hour inputs (no window); reopening an
  existing 24-Hour session opens with hours disabled. Badge current-period
  kept in sync after save.

Backend (projects.py):
- full_24h added to VALID_PERIOD_TYPES and the session-label maps
  ("... - 24-Hour"). Operator-set only; never auto-derived.
- Combined report: include ALL rows for a 24-hour session (no day/night
  window filter) and split them by hour into the three non-overlapping
  buckets — Daytime 7-18:59, Evening 19-21:59, Nighttime 22:00-06:59. Empty
  period columns are dropped downstream, so it shows whatever periods have data.

Scoped to the combined-report path; the older per-session single report still
uses the fixed Evening/Nighttime layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:37:38 +00:00
parent 5b70dcf071
commit ceb893a54c
2 changed files with 92 additions and 20 deletions
+53 -8
View File
@@ -13,12 +13,14 @@
'weekday_night': 'Weekday Night',
'weekend_day': 'Weekend Day',
'weekend_night': 'Weekend Night',
'full_24h': '24-Hour',
} %}
{% set period_colors = {
'weekday_day': 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
'weekday_night': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300',
'weekend_day': 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
'weekend_night': 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
'full_24h': 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300',
} %}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-slate-800 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
@@ -54,6 +56,7 @@
<div class="relative" id="period-wrap-{{ s.id }}">
<button onclick="openPeriodEditor('{{ s.id }}')"
id="period-badge-{{ s.id }}"
data-current-period="{{ s.period_type or '' }}"
class="px-2 py-0.5 text-xs font-medium rounded-full flex items-center gap-1 transition-colors {{ period_colors.get(s.period_type, 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400') }}"
title="Click to edit period type and hours">
<span id="period-label-{{ s.id }}">{{ period_labels.get(s.period_type, 'Set period') }}</span>
@@ -78,6 +81,12 @@
{{ pt_label }}
</button>
{% endfor %}
<button onclick="selectPeriodType('{{ s.id }}', 'full_24h')"
id="pt-btn-{{ s.id }}-full_24h"
class="period-type-btn col-span-2 text-xs py-1 px-2 rounded border transition-colors
{% if s.period_type == 'full_24h' %}border-seismo-orange bg-orange-50 text-seismo-orange dark:bg-orange-900/20{% else %}border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-gray-400{% endif %}">
24-Hour
</button>
</div>
</div>
@@ -229,14 +238,17 @@ const PERIOD_COLORS = {
weekday_night: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300',
weekend_day: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
weekend_night: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
full_24h: 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300',
};
const PERIOD_LABELS = {
weekday_day: 'Weekday Day',
weekday_night: 'Weekday Night',
weekend_day: 'Weekend Day',
weekend_night: 'Weekend Night',
full_24h: '24-Hour',
};
// Default hours for each period type
// Default hours for each period type. full_24h has no window (the report splits
// its rows by hour), so its hour inputs are cleared + disabled in the editor.
const PERIOD_DEFAULT_HOURS = {
weekday_day: {start: 7, end: 19},
weekday_night: {start: 19, end: 7},
@@ -260,6 +272,20 @@ function openPeriodEditor(sessionId) {
if (el.id !== 'period-editor-' + sessionId) el.classList.add('hidden');
});
document.getElementById('period-editor-' + sessionId).classList.toggle('hidden');
// Reflect the current period type's hour-input state on open (24-Hour
// has no window, so its hour inputs open disabled).
const cur = document.getElementById('period-badge-' + sessionId)?.dataset?.currentPeriod;
const sh = document.getElementById('period-start-hr-' + sessionId);
const eh = document.getElementById('period-end-hr-' + sessionId);
const disable = cur === 'full_24h';
[sh, eh].forEach(el => {
if (!el) return;
el.disabled = disable;
el.classList.toggle('opacity-50', disable);
el.classList.toggle('cursor-not-allowed', disable);
if (disable) el.placeholder = 'n/a';
});
}
function closePeriodEditor(sessionId) {
@@ -281,13 +307,31 @@ function selectPeriodType(sessionId, pt) {
btn.classList.toggle('text-gray-600', !isSelected);
btn.classList.toggle('dark:text-gray-400', !isSelected);
});
// Fill default hours
const defaults = PERIOD_DEFAULT_HOURS[pt];
if (defaults) {
const sh = document.getElementById('period-start-hr-' + sessionId);
const eh = document.getElementById('period-end-hr-' + sessionId);
if (sh && !sh.value) sh.value = defaults.start;
if (eh && !eh.value) eh.value = defaults.end;
// Hour inputs: full_24h has no window — clear + disable them. Other types
// re-enable and fill their default window if empty.
const sh = document.getElementById('period-start-hr-' + sessionId);
const eh = document.getElementById('period-end-hr-' + sessionId);
if (pt === 'full_24h') {
[sh, eh].forEach(el => {
if (!el) return;
el.value = '';
el.disabled = true;
el.classList.add('opacity-50', 'cursor-not-allowed');
el.placeholder = 'n/a';
});
} else {
const defaults = PERIOD_DEFAULT_HOURS[pt];
[sh, eh].forEach(el => {
if (!el) return;
el.disabled = false;
el.classList.remove('opacity-50', 'cursor-not-allowed');
});
if (sh) sh.placeholder = 'e.g. 19';
if (eh) eh.placeholder = 'e.g. 7';
if (defaults) {
if (sh && !sh.value) sh.value = defaults.start;
if (eh && !eh.value) eh.value = defaults.end;
}
}
}
@@ -315,6 +359,7 @@ async function savePeriodEditor(sessionId) {
const badge = document.getElementById('period-badge-' + sessionId);
const label = document.getElementById('period-label-' + sessionId);
const newPt = result.period_type;
badge.dataset.currentPeriod = newPt || '';
ALL_BADGE_COLORS.forEach(c => badge.classList.remove(c));
if (newPt && PERIOD_COLORS[newPt]) {
badge.classList.add(...PERIOD_COLORS[newPt].split(' ').filter(Boolean));