From 870bc0649b71f7b69abfdd34e0a5a3ef672de314 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sun, 28 Jun 2026 08:35:39 +0000 Subject: [PATCH] Emergency fixes on prod --- backend/app/crud/crud_local_flight.py | 4 ++- backend/tests/test_flight_strip_apis.py | 35 +++++++++++++++++++++ web/index.html | 11 +++++-- web/shared.js | 41 ++++++++++++++++--------- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/backend/app/crud/crud_local_flight.py b/backend/app/crud/crud_local_flight.py index d436167..31924b2 100644 --- a/backend/app/crud/crud_local_flight.py +++ b/backend/app/crud/crud_local_flight.py @@ -187,7 +187,7 @@ class CRUDLocalFlight: current_time = timestamp if timestamp is not None else datetime.utcnow() if status == LocalFlightStatus.GROUND: db_obj.contact_dt = current_time - elif status == LocalFlightStatus.DEPARTED: + elif status == LocalFlightStatus.DEPARTED and not db_obj.departed_dt: db_obj.departed_dt = current_time elif status == LocalFlightStatus.LANDED and not db_obj.landed_dt: db_obj.landed_dt = current_time @@ -200,6 +200,8 @@ class CRUDLocalFlight: # Takeoff: happens once when transitioning away from GROUND if old_status == LocalFlightStatus.GROUND and status in (LocalFlightStatus.DEPARTED, LocalFlightStatus.LOCAL, LocalFlightStatus.CIRCUIT) and not db_obj.takeoff_dt: db_obj.takeoff_dt = current_time + if not db_obj.departed_dt: + db_obj.departed_dt = current_time db.add(db_obj) db.commit() diff --git a/backend/tests/test_flight_strip_apis.py b/backend/tests/test_flight_strip_apis.py index 088ce8f..5c595f3 100644 --- a/backend/tests/test_flight_strip_apis.py +++ b/backend/tests/test_flight_strip_apis.py @@ -194,6 +194,7 @@ def test_local_flight_lifecycle_special_lists_and_not_found_paths(auth_client, d assert departed_response.status_code == 200 assert departed_response.json()["takeoff_dt"] == "2026-06-20T10:05:00" + assert departed_response.json()["departed_dt"] == "2026-06-20T10:05:00" assert landed_response.status_code == 200 assert landed_response.json()["landed_dt"] == "2026-06-20T10:45:00" @@ -222,6 +223,40 @@ def test_local_flight_lifecycle_special_lists_and_not_found_paths(auth_client, d assert auth_client.delete("/api/v1/local-flights/404").status_code == 404 +def test_local_flight_takeoff_to_local_sets_departed_dt(auth_client): + create_response = auth_client.post( + "/api/v1/local-flights/", + json={ + "registration": "g-air", + "type": "PA28", + "pob": 2, + "flight_type": "LOCAL", + "duration": 30, + "etd": "2026-06-20T09:00:00", + }, + ) + assert create_response.status_code == 200 + + takeoff_response = auth_client.patch( + f"/api/v1/local-flights/{create_response.json()['id']}/status", + json={"status": "LOCAL", "timestamp": "2026-06-20T09:05:00"}, + ) + + assert takeoff_response.status_code == 200 + assert takeoff_response.json()["status"] == "LOCAL" + assert takeoff_response.json()["takeoff_dt"] == "2026-06-20T09:05:00" + assert takeoff_response.json()["departed_dt"] == "2026-06-20T09:05:00" + + landing_response = auth_client.patch( + f"/api/v1/local-flights/{create_response.json()['id']}/status", + json={"status": "LANDED", "timestamp": "2026-06-20T09:35:00"}, + ) + + assert landing_response.status_code == 200 + assert landing_response.json()["status"] == "LANDED" + assert landing_response.json()["landed_dt"] == "2026-06-20T09:35:00" + + def test_overflight_lifecycle_special_lists_and_not_found_paths(auth_client, db): payload = { "registration": "g-ovr", diff --git a/web/index.html b/web/index.html index 85717ca..8994937 100644 --- a/web/index.html +++ b/web/index.html @@ -631,8 +631,15 @@ if (!utcDateTimeString) return ''; try { - // Parse the ISO datetime string - const date = new Date(utcDateTimeString); + // API datetimes are UTC; normalize naive strings before local display. + let utcDateStr = utcDateTimeString; + if (!utcDateStr.includes('T')) { + utcDateStr = utcDateStr.replace(' ', 'T'); + } + if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(utcDateStr)) { + utcDateStr += 'Z'; + } + const date = new Date(utcDateStr); // Check if valid date if (isNaN(date.getTime())) { diff --git a/web/shared.js b/web/shared.js index 0277bfc..4094ecc 100644 --- a/web/shared.js +++ b/web/shared.js @@ -619,6 +619,21 @@ return date.toISOString().slice(0, 10) + ' ' + date.toISOString().slice(11, 16); } + function normalizeUtcDateString(dateStr) { + let utcDateStr = dateStr; + if (!utcDateStr.includes('T')) { + utcDateStr = utcDateStr.replace(' ', 'T'); + } + if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(utcDateStr)) { + utcDateStr += 'Z'; + } + return utcDateStr; + } + + function utcInputToIso(dateStr, timeStr) { + return `${dateStr}T${timeStr}:00Z`; + } + // Modal functions function openNewPPRModal() { isNewPPR = true; @@ -755,13 +770,7 @@ if (key === 'eta' || key === 'etd') { if (ppr[key]) { // ppr[key] is UTC datetime string from API (naive, assume UTC) - let utcDateStr = ppr[key]; - if (!utcDateStr.includes('T')) { - utcDateStr = utcDateStr.replace(' ', 'T'); - } - if (!utcDateStr.includes('Z')) { - utcDateStr += 'Z'; - } + const utcDateStr = normalizeUtcDateString(ppr[key]); const date = new Date(utcDateStr); // Now correctly parsed as UTC // Split into date and time components for separate inputs @@ -770,15 +779,15 @@ if (dateField && timeField) { // Format date - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); const dateValue = `${year}-${month}-${day}`; dateField.value = dateValue; // Format time (round to nearest 15-minute interval) - const hours = String(date.getHours()).padStart(2, '0'); - const rawMinutes = date.getMinutes(); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const rawMinutes = date.getUTCMinutes(); const roundedMinutes = Math.round(rawMinutes / 15) * 15 % 60; const minutes = String(roundedMinutes).padStart(2, '0'); const timeValue = `${hours}:${minutes}`; @@ -1089,12 +1098,16 @@ // Combine date and time for ETA const dateStr = formData.get('eta-date'); const timeStr = formData.get('eta-time'); - pprData.eta = new Date(`${dateStr}T${timeStr}`).toISOString(); + pprData.eta = isNewPPR + ? new Date(`${dateStr}T${timeStr}`).toISOString() + : utcInputToIso(dateStr, timeStr); } else if (key === 'etd-date' && formData.get('etd-time')) { // Combine date and time for ETD const dateStr = formData.get('etd-date'); const timeStr = formData.get('etd-time'); - pprData.etd = new Date(`${dateStr}T${timeStr}`).toISOString(); + pprData.etd = isNewPPR + ? new Date(`${dateStr}T${timeStr}`).toISOString() + : utcInputToIso(dateStr, timeStr); } else if (key !== 'eta-time' && key !== 'etd-time') { // Skip the time fields as they're handled above pprData[key] = value;