Update to 0.9.3 #43
@@ -20,7 +20,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from backend.utils.timezone import utc_to_local, format_local_datetime
|
from backend.utils.timezone import utc_to_local, format_local_datetime, local_to_utc
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
from fastapi import UploadFile, File
|
from fastapi import UploadFile, File
|
||||||
@@ -1800,6 +1800,23 @@ async def delete_session(
|
|||||||
|
|
||||||
VALID_PERIOD_TYPES = {"weekday_day", "weekday_night", "weekend_day", "weekend_night"}
|
VALID_PERIOD_TYPES = {"weekday_day", "weekday_night", "weekend_day", "weekend_night"}
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_period_type(dt: datetime) -> str:
|
||||||
|
is_weekend = dt.weekday() >= 5
|
||||||
|
is_night = dt.hour >= 22 or dt.hour < 7
|
||||||
|
if is_weekend:
|
||||||
|
return "weekend_night" if is_night else "weekend_day"
|
||||||
|
return "weekday_night" if is_night else "weekday_day"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_session_label(dt: datetime, location_name: str, period_type: str) -> str:
|
||||||
|
day_abbr = dt.strftime("%a")
|
||||||
|
date_str = f"{dt.month}/{dt.day}"
|
||||||
|
period_str = {"weekday_day": "Day", "weekday_night": "Night", "weekend_day": "Day", "weekend_night": "Night"}.get(period_type, "")
|
||||||
|
parts = [p for p in [location_name, f"{day_abbr} {date_str}", period_str] if p]
|
||||||
|
return " — ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{project_id}/sessions/{session_id}")
|
@router.patch("/{project_id}/sessions/{session_id}")
|
||||||
async def patch_session(
|
async def patch_session(
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@@ -1807,13 +1824,53 @@ async def patch_session(
|
|||||||
data: dict,
|
data: dict,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update session_label and/or period_type on a monitoring session."""
|
"""Update session fields: started_at, stopped_at, session_label, period_type."""
|
||||||
session = db.query(MonitoringSession).filter_by(id=session_id).first()
|
session = db.query(MonitoringSession).filter_by(id=session_id).first()
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
if session.project_id != project_id:
|
if session.project_id != project_id:
|
||||||
raise HTTPException(status_code=403, detail="Session does not belong to this project")
|
raise HTTPException(status_code=403, detail="Session does not belong to this project")
|
||||||
|
|
||||||
|
times_changed = False
|
||||||
|
|
||||||
|
if "started_at" in data and data["started_at"]:
|
||||||
|
try:
|
||||||
|
local_dt = datetime.fromisoformat(data["started_at"])
|
||||||
|
session.started_at = local_to_utc(local_dt)
|
||||||
|
times_changed = True
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid started_at datetime format")
|
||||||
|
|
||||||
|
if "stopped_at" in data:
|
||||||
|
if data["stopped_at"]:
|
||||||
|
try:
|
||||||
|
local_dt = datetime.fromisoformat(data["stopped_at"])
|
||||||
|
session.stopped_at = local_to_utc(local_dt)
|
||||||
|
times_changed = True
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid stopped_at datetime format")
|
||||||
|
else:
|
||||||
|
session.stopped_at = None
|
||||||
|
times_changed = True
|
||||||
|
|
||||||
|
if times_changed and session.started_at and session.stopped_at:
|
||||||
|
delta = session.stopped_at - session.started_at
|
||||||
|
session.duration_seconds = max(0, int(delta.total_seconds()))
|
||||||
|
elif times_changed and not session.stopped_at:
|
||||||
|
session.duration_seconds = None
|
||||||
|
|
||||||
|
# Re-derive period_type and session_label from new started_at unless explicitly provided
|
||||||
|
if times_changed and session.started_at and "period_type" not in data:
|
||||||
|
local_start = utc_to_local(session.started_at)
|
||||||
|
session.period_type = _derive_period_type(local_start)
|
||||||
|
|
||||||
|
if times_changed and session.started_at and "session_label" not in data:
|
||||||
|
from backend.models import MonitoringLocation
|
||||||
|
location = db.query(MonitoringLocation).filter_by(id=session.location_id).first()
|
||||||
|
location_name = location.name if location else ""
|
||||||
|
local_start = utc_to_local(session.started_at)
|
||||||
|
session.session_label = _build_session_label(local_start, location_name, session.period_type or "")
|
||||||
|
|
||||||
if "session_label" in data:
|
if "session_label" in data:
|
||||||
session.session_label = str(data["session_label"]).strip() or None
|
session.session_label = str(data["session_label"]).strip() or None
|
||||||
if "period_type" in data:
|
if "period_type" in data:
|
||||||
|
|||||||
@@ -56,6 +56,15 @@
|
|||||||
{% else %}bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300{% endif %}">
|
{% else %}bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300{% endif %}">
|
||||||
{{ session.status or 'unknown' }}
|
{{ session.status or 'unknown' }}
|
||||||
</span>
|
</span>
|
||||||
|
<!-- Edit Session Times -->
|
||||||
|
<button onclick="event.stopPropagation(); openEditSessionModal('{{ session.id }}', '{{ session.started_at|local_datetime if session.started_at else '' }}', '{{ session.stopped_at|local_datetime if session.stopped_at else '' }}')"
|
||||||
|
class="px-3 py-1 text-xs bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-1"
|
||||||
|
title="Edit session times">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||||
|
</svg>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
<!-- Download All Files in Session -->
|
<!-- Download All Files in Session -->
|
||||||
<button onclick="event.stopPropagation(); downloadSessionFiles('{{ session.id }}')"
|
<button onclick="event.stopPropagation(); downloadSessionFiles('{{ session.id }}')"
|
||||||
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors flex items-center gap-1"
|
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors flex items-center gap-1"
|
||||||
@@ -326,4 +335,74 @@ async function deleteSession(sessionId) {
|
|||||||
alert(`Error deleting session: ${error.message}`);
|
alert(`Error deleting session: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditSessionModal(sessionId, startedAt, stoppedAt) {
|
||||||
|
document.getElementById('editSessionId').value = sessionId;
|
||||||
|
// local_datetime filter returns "YYYY-MM-DD HH:MM" — convert to "YYYY-MM-DDTHH:MM" for datetime-local input
|
||||||
|
document.getElementById('editStartedAt').value = startedAt ? startedAt.replace(' ', 'T') : '';
|
||||||
|
document.getElementById('editStoppedAt').value = stoppedAt ? stoppedAt.replace(' ', 'T') : '';
|
||||||
|
document.getElementById('editSessionModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditSessionModal() {
|
||||||
|
document.getElementById('editSessionModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSessionTimes() {
|
||||||
|
const sessionId = document.getElementById('editSessionId').value;
|
||||||
|
const startedAt = document.getElementById('editStartedAt').value;
|
||||||
|
const stoppedAt = document.getElementById('editStoppedAt').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/projects/{{ project_id }}/sessions/${sessionId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
started_at: startedAt || null,
|
||||||
|
stopped_at: stoppedAt || null,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeEditSessionModal();
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(`Failed to update session: ${data.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Error updating session: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Edit Session Times Modal -->
|
||||||
|
<div id="editSessionModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm mx-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit Session Times</h3>
|
||||||
|
<input type="hidden" id="editSessionId">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Start Time</label>
|
||||||
|
<input type="datetime-local" id="editStartedAt"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Stop Time</label>
|
||||||
|
<input type="datetime-local" id="editStoppedAt"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">Times are in your local timezone. The session label and period type will be updated automatically.</p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button onclick="closeEditSessionModal()"
|
||||||
|
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button onclick="saveSessionTimes()"
|
||||||
|
class="px-4 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user