- 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>
423 lines
23 KiB
HTML
423 lines
23 KiB
HTML
<!-- Monitoring Sessions List -->
|
||
{% if sessions %}
|
||
<div class="space-y-3">
|
||
{% for item in sessions %}
|
||
{% set s = item.session %}
|
||
{% set loc = item.location %}
|
||
{% set unit = item.unit %}
|
||
{% set effective_range = item.effective_range %}
|
||
|
||
{# Period display maps #}
|
||
{% set period_labels = {
|
||
'weekday_day': 'Weekday Day',
|
||
'weekday_night': 'Weekday Night',
|
||
'weekend_day': 'Weekend Day',
|
||
'weekend_night': 'Weekend Night',
|
||
} %}
|
||
{% 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',
|
||
} %}
|
||
|
||
<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"
|
||
id="session-card-{{ s.id }}">
|
||
<div class="flex items-start justify-between gap-3 p-4 pb-3">
|
||
<div class="min-w-0 flex-1">
|
||
|
||
<!-- Label + badges -->
|
||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||
<span id="label-display-{{ s.id }}"
|
||
class="font-semibold text-gray-900 dark:text-white text-sm cursor-pointer hover:text-seismo-orange"
|
||
title="Click to edit label"
|
||
onclick="startEditLabel('{{ s.id }}')">
|
||
{{ s.session_label or ('Session ' + s.id[:8] + '…') }}
|
||
</span>
|
||
<input id="label-input-{{ s.id }}"
|
||
class="hidden text-sm font-semibold bg-transparent border-b border-seismo-orange text-gray-900 dark:text-white focus:outline-none min-w-[180px]"
|
||
value="{{ s.session_label or '' }}"
|
||
onblur="saveLabel('{{ s.id }}')"
|
||
onkeydown="if(event.key==='Enter'){saveLabel('{{ s.id }}');}if(event.key==='Escape'){cancelEditLabel('{{ s.id }}');}">
|
||
|
||
{% if s.status == 'recording' %}
|
||
<span class="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 rounded-full flex items-center gap-1">
|
||
<span class="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse"></span>Recording
|
||
</span>
|
||
{% elif s.status == 'completed' %}
|
||
<span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Completed</span>
|
||
{% elif s.status == 'failed' %}
|
||
<span class="px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400 rounded-full">Failed</span>
|
||
{% endif %}
|
||
|
||
<!-- Period type badge (click to open hour editor) -->
|
||
<div class="relative" id="period-wrap-{{ s.id }}">
|
||
<button onclick="openPeriodEditor('{{ s.id }}')"
|
||
id="period-badge-{{ s.id }}"
|
||
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>
|
||
<svg class="w-3 h-3 opacity-60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Period editor panel -->
|
||
<div id="period-editor-{{ s.id }}"
|
||
class="hidden absolute left-0 top-full mt-1 z-20 bg-white dark:bg-slate-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg w-64 p-3 space-y-3">
|
||
|
||
<!-- Period type selector -->
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Period Type</label>
|
||
<div class="grid grid-cols-2 gap-1">
|
||
{% for pt, pt_label in [('weekday_day','WD Day'),('weekday_night','WD Night'),('weekend_day','WE Day'),('weekend_night','WE Night')] %}
|
||
<button onclick="selectPeriodType('{{ s.id }}', '{{ pt }}')"
|
||
id="pt-btn-{{ s.id }}-{{ pt }}"
|
||
class="period-type-btn text-xs py-1 px-2 rounded border transition-colors
|
||
{% if s.period_type == pt %}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 %}">
|
||
{{ pt_label }}
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hour inputs -->
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Start Hour (0–23)</label>
|
||
<input type="number" min="0" max="23"
|
||
id="period-start-hr-{{ s.id }}"
|
||
value="{{ s.period_start_hour if s.period_start_hour is not none else '' }}"
|
||
placeholder="e.g. 19"
|
||
class="w-full text-xs bg-gray-50 dark:bg-slate-600 border border-gray-200 dark:border-gray-500 rounded px-2 py-1 text-gray-800 dark:text-gray-200 focus:outline-none focus:border-seismo-orange">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">End Hour (0–23)</label>
|
||
<input type="number" min="0" max="23"
|
||
id="period-end-hr-{{ s.id }}"
|
||
value="{{ s.period_end_hour if s.period_end_hour is not none else '' }}"
|
||
placeholder="e.g. 7"
|
||
class="w-full text-xs bg-gray-50 dark:bg-slate-600 border border-gray-200 dark:border-gray-500 rounded px-2 py-1 text-gray-800 dark:text-gray-200 focus:outline-none focus:border-seismo-orange">
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-400 dark:text-gray-500">Day: 7→19 · Night: 19→7 · Customize as needed</p>
|
||
|
||
<!-- Actions -->
|
||
<div class="flex gap-2 pt-1">
|
||
<button onclick="savePeriodEditor('{{ s.id }}')"
|
||
class="flex-1 text-xs py-1 bg-seismo-orange text-white rounded hover:bg-orange-600 transition-colors">
|
||
Save
|
||
</button>
|
||
<button onclick="closePeriodEditor('{{ s.id }}')"
|
||
class="text-xs py-1 px-2 border border-gray-200 dark:border-gray-600 rounded text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-600 transition-colors">
|
||
Cancel
|
||
</button>
|
||
<button onclick="clearPeriodEditor('{{ s.id }}')"
|
||
class="text-xs py-1 px-2 border border-gray-200 dark:border-gray-600 rounded text-gray-500 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-600 transition-colors"
|
||
title="Clear period type and hours">
|
||
Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Info grid -->
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||
{% if loc %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||
</svg>
|
||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ loc.name }}</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if s.started_at %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||
</svg>
|
||
<span>{{ s.started_at|local_datetime }}</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if s.stopped_at %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||
</svg>
|
||
<span>Ended {{ s.stopped_at|local_datetime }}</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if s.duration_seconds %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||
</svg>
|
||
<span>{{ (s.duration_seconds // 3600) }}h {{ ((s.duration_seconds % 3600) // 60) }}m</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if unit %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2v-4M9 21H5a2 2 0 01-2-2v-4m0 0h18"></path>
|
||
</svg>
|
||
<a href="/slm/{{ unit.id }}?from_project={{ project_id }}"
|
||
class="text-seismo-orange hover:underline font-medium">{{ unit.id }}</a>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if s.device_model %}
|
||
<div class="flex items-center gap-1">
|
||
<svg class="w-3.5 h-3.5 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
|
||
</svg>
|
||
<span>{{ s.device_model }}</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Effective window (when period hours are set) -->
|
||
{% if effective_range %}
|
||
<div class="flex items-center gap-1 mt-1.5 text-xs text-indigo-600 dark:text-indigo-400">
|
||
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||
</svg>
|
||
<span id="effective-range-{{ s.id }}">Effective: {{ effective_range }}</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="hidden text-xs text-indigo-600 dark:text-indigo-400 mt-1.5"
|
||
id="effective-range-{{ s.id }}"></div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 shrink-0">
|
||
{% if s.status == 'recording' %}
|
||
<button onclick="stopRecording('{{ s.id }}')"
|
||
class="px-3 py-1 text-xs bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
|
||
Stop
|
||
</button>
|
||
{% endif %}
|
||
<button onclick="viewSession('{{ s.id }}')"
|
||
class="px-3 py-1 text-xs bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||
Details
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-12">
|
||
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path>
|
||
</svg>
|
||
<p class="text-gray-500 dark:text-gray-400 mb-1">No monitoring sessions yet</p>
|
||
<p class="text-sm text-gray-400 dark:text-gray-500">Upload data to create sessions</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<script>
|
||
const PROJECT_ID = '{{ project_id }}';
|
||
|
||
const 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',
|
||
};
|
||
const PERIOD_LABELS = {
|
||
weekday_day: 'Weekday Day',
|
||
weekday_night: 'Weekday Night',
|
||
weekend_day: 'Weekend Day',
|
||
weekend_night: 'Weekend Night',
|
||
};
|
||
// Default hours for each period type
|
||
const PERIOD_DEFAULT_HOURS = {
|
||
weekday_day: {start: 7, end: 19},
|
||
weekday_night: {start: 19, end: 7},
|
||
weekend_day: {start: 7, end: 19},
|
||
weekend_night: {start: 19, end: 7},
|
||
};
|
||
const FALLBACK_COLORS = ['bg-gray-100','text-gray-500','dark:bg-gray-700','dark:text-gray-400'];
|
||
const ALL_BADGE_COLORS = [...new Set([
|
||
...FALLBACK_COLORS,
|
||
...Object.values(PERIOD_COLORS).flatMap(s => s.split(' '))
|
||
])];
|
||
|
||
// Track which period type is selected in the editor before saving
|
||
const _editorState = {};
|
||
|
||
// ---- Period editor ----
|
||
|
||
function openPeriodEditor(sessionId) {
|
||
// Close all other editors first
|
||
document.querySelectorAll('[id^="period-editor-"]').forEach(el => {
|
||
if (el.id !== 'period-editor-' + sessionId) el.classList.add('hidden');
|
||
});
|
||
document.getElementById('period-editor-' + sessionId).classList.toggle('hidden');
|
||
}
|
||
|
||
function closePeriodEditor(sessionId) {
|
||
document.getElementById('period-editor-' + sessionId).classList.add('hidden');
|
||
delete _editorState[sessionId];
|
||
}
|
||
|
||
function selectPeriodType(sessionId, pt) {
|
||
_editorState[sessionId] = pt;
|
||
// Highlight selected button
|
||
document.querySelectorAll(`[id^="pt-btn-${sessionId}-"]`).forEach(btn => {
|
||
const isSelected = btn.id === `pt-btn-${sessionId}-${pt}`;
|
||
btn.classList.toggle('border-seismo-orange', isSelected);
|
||
btn.classList.toggle('bg-orange-50', isSelected);
|
||
btn.classList.toggle('text-seismo-orange', isSelected);
|
||
btn.classList.toggle('dark:bg-orange-900/20', isSelected);
|
||
btn.classList.toggle('border-gray-200', !isSelected);
|
||
btn.classList.toggle('dark:border-gray-600', !isSelected);
|
||
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;
|
||
}
|
||
}
|
||
|
||
async function savePeriodEditor(sessionId) {
|
||
const pt = _editorState[sessionId] || document.getElementById('period-badge-' + sessionId)
|
||
?.dataset?.currentPeriod || null;
|
||
const shInput = document.getElementById('period-start-hr-' + sessionId);
|
||
const ehInput = document.getElementById('period-end-hr-' + sessionId);
|
||
|
||
const payload = {};
|
||
if (pt !== undefined) payload.period_type = pt || null;
|
||
payload.period_start_hour = shInput?.value !== '' ? parseInt(shInput.value, 10) : null;
|
||
payload.period_end_hour = ehInput?.value !== '' ? parseInt(ehInput.value, 10) : null;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/projects/${PROJECT_ID}/sessions/${sessionId}`, {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) throw new Error(await resp.text());
|
||
const result = await resp.json();
|
||
|
||
// Update badge
|
||
const badge = document.getElementById('period-badge-' + sessionId);
|
||
const label = document.getElementById('period-label-' + sessionId);
|
||
const newPt = result.period_type;
|
||
ALL_BADGE_COLORS.forEach(c => badge.classList.remove(c));
|
||
if (newPt && PERIOD_COLORS[newPt]) {
|
||
badge.classList.add(...PERIOD_COLORS[newPt].split(' ').filter(Boolean));
|
||
if (label) label.textContent = PERIOD_LABELS[newPt];
|
||
} else {
|
||
badge.classList.add(...FALLBACK_COLORS);
|
||
if (label) label.textContent = 'Set period';
|
||
}
|
||
|
||
// Update effective range display
|
||
_updateEffectiveRange(sessionId, result.period_start_hour, result.period_end_hour);
|
||
|
||
closePeriodEditor(sessionId);
|
||
} catch (err) {
|
||
alert('Failed to save period: ' + err.message);
|
||
}
|
||
}
|
||
|
||
async function clearPeriodEditor(sessionId) {
|
||
const shInput = document.getElementById('period-start-hr-' + sessionId);
|
||
const ehInput = document.getElementById('period-end-hr-' + sessionId);
|
||
if (shInput) shInput.value = '';
|
||
if (ehInput) ehInput.value = '';
|
||
_editorState[sessionId] = null;
|
||
|
||
// Reset period type button highlights
|
||
document.querySelectorAll(`[id^="pt-btn-${sessionId}-"]`).forEach(btn => {
|
||
btn.classList.remove('border-seismo-orange','bg-orange-50','text-seismo-orange','dark:bg-orange-900/20');
|
||
btn.classList.add('border-gray-200','dark:border-gray-600','text-gray-600','dark:text-gray-400');
|
||
});
|
||
}
|
||
|
||
// ---- Effective range helper ----
|
||
|
||
function _updateEffectiveRange(sessionId, startHour, endHour) {
|
||
const el = document.getElementById('effective-range-' + sessionId);
|
||
if (!el) return;
|
||
if (startHour == null || endHour == null) {
|
||
el.textContent = '';
|
||
el.classList.add('hidden');
|
||
return;
|
||
}
|
||
function _fmt(h) {
|
||
const ampm = h < 12 ? 'AM' : 'PM';
|
||
const h12 = h % 12 || 12;
|
||
return `${h12}:00 ${ampm}`;
|
||
}
|
||
// We don't have the session start date in JS so just show the hours pattern
|
||
el.textContent = `Effective window: ${_fmt(startHour)} → ${_fmt(endHour)}`;
|
||
el.classList.remove('hidden');
|
||
}
|
||
|
||
// ---- Close editors on outside click ----
|
||
document.addEventListener('click', function(e) {
|
||
if (!e.target.closest('[id^="period-wrap-"]')) {
|
||
document.querySelectorAll('[id^="period-editor-"]').forEach(m => m.classList.add('hidden'));
|
||
}
|
||
});
|
||
|
||
// ---- Label editing ----
|
||
|
||
function startEditLabel(sessionId) {
|
||
document.getElementById('label-display-' + sessionId).classList.add('hidden');
|
||
const input = document.getElementById('label-input-' + sessionId);
|
||
input.classList.remove('hidden');
|
||
input.focus();
|
||
input.select();
|
||
}
|
||
|
||
function cancelEditLabel(sessionId) {
|
||
document.getElementById('label-input-' + sessionId).classList.add('hidden');
|
||
document.getElementById('label-display-' + sessionId).classList.remove('hidden');
|
||
}
|
||
|
||
async function saveLabel(sessionId) {
|
||
const display = document.getElementById('label-display-' + sessionId);
|
||
const input = document.getElementById('label-input-' + sessionId);
|
||
const newLabel = input.value.trim();
|
||
try {
|
||
const resp = await fetch(`/api/projects/${PROJECT_ID}/sessions/${sessionId}`, {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({session_label: newLabel}),
|
||
});
|
||
if (!resp.ok) throw new Error(await resp.text());
|
||
display.textContent = newLabel || ('Session ' + sessionId.slice(0, 8) + '…');
|
||
} catch(err) {
|
||
alert('Failed to save label: ' + err.message);
|
||
} finally {
|
||
input.classList.add('hidden');
|
||
display.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// ---- Session details ----
|
||
|
||
function viewSession(sessionId) {
|
||
window.location.href = `/api/projects/${PROJECT_ID}/sessions/${sessionId}/detail`;
|
||
}
|
||
|
||
function stopRecording(sessionId) {
|
||
if (!confirm('Stop this monitoring session?')) return;
|
||
alert('Stop recording API coming soon for session: ' + sessionId);
|
||
}
|
||
</script>
|