From 8eefe5a91aaf622205763db70039c80df70cb669 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 18:25:48 +0000 Subject: [PATCH 1/2] fix: anchor check-in and gate closure times to scheduled departure Closure times (check-in open/close, gate open/close) must be computed from the scheduled departure time (STD) so they never drift when a flight is delayed. Previously getBestDepartureTs was used, which follows the real/estimated time and would silently shift all closure timestamps whenever an ETD update arrived. Changes: - FlightScreen widget: filter and map now use getScheduledFlightTs for closure time overlap checks and ciOpen/ciClose/gateOpen/gateClose values; departureTs/departureTime still use ETD for correct display. - autoNotifications: closure notification timestamps (CI open/close, gate open/close) computed from STD; departure display text still shows ETD. - flightNotificationScheduler: pinned closure-phase notifications use STD as base; only the pinned_departure phase keeps ETD. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GwhK5jVxWTojXNS8qTQ4D3 --- src/screens/FlightScreen.tsx | 21 +++++++------- src/utils/autoNotifications.ts | 35 ++++++++++++------------ src/utils/flightNotificationScheduler.ts | 24 +++++++++------- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index de34c36d..bf70c36a 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -33,7 +33,7 @@ import { requestShiftWidgetUpdate } from '../widgets/widgetThemeSync'; import { useLanguage } from '../context/LanguageContext'; import type { TranslationKey } from '../i18n/translations'; import { dismissPinnedFlightNotification, showOrUpdatePinnedFlightNotification } from '../utils/pinnedFlightOngoingNotification'; -import { getBestArrivalTs, getBestDepartureTs } from '../utils/flightTimes'; +import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from '../utils/flightTimes'; import { isFlightEasyJet } from '../utils/easyjetOverlapMode'; import { compareFlightsChronologically, @@ -894,17 +894,18 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean const wAllowedAirlines: string[] = wFilterRaw ? JSON.parse(wFilterRaw) : []; const wFlights: WidgetFlight[] = mergedDeps .filter(item => { - const ts = getBestDepartureTs(item); - if (ts == null) return false; + const stdTs = getScheduledFlightTs(item, 'departure'); + if (stdTs == null) return false; const airline = item.flight?.airline?.name || ''; if (wAllowedAirlines.length > 0 && !wAllowedAirlines.some(k => isFlightAirlineMatch(item, k))) return false; const ops = getAirlineOps(airline); - const ciO = ts - ops.checkInOpen * 60, ciC = ts - ops.checkInClose * 60; - const gO = ts - ops.gateOpen * 60, gC = ts - ops.gateClose * 60; + const ciO = stdTs - ops.checkInOpen * 60, ciC = stdTs - ops.checkInClose * 60; + const gO = stdTs - ops.gateOpen * 60, gC = stdTs - ops.gateClose * 60; return (ciO <= activeWidgetShift.end && ciC >= activeWidgetShift.start) || (gO <= activeWidgetShift.end && gC >= activeWidgetShift.start); }) .map(item => { - const ts = getBestDepartureTs(item)!; + const etdTs = getBestDepartureTs(item)!; + const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs; const airline = item.flight?.airline?.name || 'Sconosciuta'; const airlineIdentity = [ airline, @@ -921,10 +922,10 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean return { flightNumber: fn, destinationIata: getFlightAirportLabel(item.flight?.airport?.destination, 'N/A'), - departureTs: ts, - departureTime: fmtT(ts), - ciOpen: fmtOff(ts, ops.checkInOpen), ciClose: fmtOff(ts, ops.checkInClose), - gateOpen: fmtOff(ts, ops.gateOpen), gateClose: fmtOff(ts, ops.gateClose), + departureTs: etdTs, + departureTime: fmtT(etdTs), + ciOpen: fmtOff(stdTs, ops.checkInOpen), ciClose: fmtOff(stdTs, ops.checkInClose), + gateOpen: fmtOff(stdTs, ops.gateOpen), gateClose: fmtOff(stdTs, ops.gateClose), airlineColor: getAirlineColor(airlineIdentity), isPinned: fn === pinnedFn, stand: sm?.stand, diff --git a/src/utils/autoNotifications.ts b/src/utils/autoNotifications.ts index 95e15e46..5ed9fd06 100644 --- a/src/utils/autoNotifications.ts +++ b/src/utils/autoNotifications.ts @@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { getAirlineOps } from './airlineOps'; import { fetchAirportScheduleRaw } from './fr24api'; import { getFlightAirportLabel, isFlightAirlineMatch } from './flightScheduleAdapter'; -import { getBestArrivalTs, getBestDepartureTs } from './flightTimes'; +import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from './flightTimes'; import { showShiftOngoingNotification, dismissShiftOngoingNotification, @@ -237,22 +237,23 @@ export async function autoScheduleNotifications(): Promise { // ── Departure notifications: check-in/gate open-close warnings ── for (const item of shiftDepartures) { try { - const depTs = getBestDepartureTs(item); - if (!depTs || isNaN(depTs)) continue; + const etdTs = getBestDepartureTs(item); + const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs; + if (!stdTs || isNaN(stdTs)) continue; const airline = item.flight?.airline?.name || 'Sconosciuta'; const flightNumber = item.flight?.identification?.number?.default || 'N/A'; const destination = getFlightAirportLabel(item.flight?.airport?.destination, 'N/A'); - const depTime = new Date(depTs * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); + const depTime = new Date((etdTs ?? stdTs) * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); // Get airline-specific ops times const ops = getAirlineOps(airline); - // Check-in open/close timestamps - const ciOpenTs = depTs - ops.checkInOpen * 60; - const ciCloseTs = depTs - ops.checkInClose * 60; - const gateOpenTs = depTs - ops.gateOpen * 60; - const gateCloseTs = depTs - ops.gateClose * 60; + // Closure timestamps anchored to scheduled departure — never shift with delays + const ciOpenTs = stdTs - ops.checkInOpen * 60; + const ciCloseTs = stdTs - ops.checkInClose * 60; + const gateOpenTs = stdTs - ops.gateOpen * 60; + const gateCloseTs = stdTs - ops.gateClose * 60; // Notification 10 min before check-in open const secondsUntilCIOpenWarn = ciOpenTs - 10 * 60 - now; @@ -267,8 +268,8 @@ export async function autoScheduleNotifications(): Promise { scheduler: 'auto', type: 'checkin_open_10min', flightNumber, - ts: depTs, - extra: { depTs }, + ts: stdTs, + extra: { depTs: stdTs }, }), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCIOpenWarn), repeats: false }, @@ -289,8 +290,8 @@ export async function autoScheduleNotifications(): Promise { scheduler: 'auto', type: 'checkin_close_10min', flightNumber, - ts: depTs, - extra: { depTs }, + ts: stdTs, + extra: { depTs: stdTs }, }), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCICloseWarn), repeats: false }, @@ -311,8 +312,8 @@ export async function autoScheduleNotifications(): Promise { scheduler: 'auto', type: 'gate_open_5min', flightNumber, - ts: depTs, - extra: { depTs }, + ts: stdTs, + extra: { depTs: stdTs }, }), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGateOpenWarn), repeats: false }, @@ -333,8 +334,8 @@ export async function autoScheduleNotifications(): Promise { scheduler: 'auto', type: 'gate_close_5min', flightNumber, - ts: depTs, - extra: { depTs }, + ts: stdTs, + extra: { depTs: stdTs }, }), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGateCloseWarn), repeats: false }, diff --git a/src/utils/flightNotificationScheduler.ts b/src/utils/flightNotificationScheduler.ts index aa72461e..ad9d86d3 100644 --- a/src/utils/flightNotificationScheduler.ts +++ b/src/utils/flightNotificationScheduler.ts @@ -2,7 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Notifications from 'expo-notifications'; import { getAirlineOps } from './airlineOps'; import { getFlightAirportLabel } from './flightScheduleAdapter'; -import { getBestArrivalTs, getBestDepartureTs } from './flightTimes'; +import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from './flightTimes'; import { shouldNotifyAirline, type FlightNotificationSettings } from './flightNotificationSettings'; import { isFlightEasyJet } from './easyjetOverlapMode'; import { @@ -229,26 +229,30 @@ export async function schedulePinnedNotifications( ids.push(id); } } else { - const ts = getBestDepartureTs(item); - if (!ts) return; + const etdTs = getBestDepartureTs(item); + if (!etdTs) return; + const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs; const dest = getFlightAirportLabel(item.flight?.airport?.destination, 'N/A'); - const depTime = new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); + const depTime = new Date(etdTs * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); const ops = getAirlineOps(airline); - const phases: Array<{ offset: number; type: string; title: string; body: string }> = [ - { offset: ops.checkInOpen, type: 'pinned_checkin_open', title: `Check-in aperto - ${flightNumber}`, body: `Check-in aperto per il volo delle ${depTime} → ${dest}` }, - { offset: ops.gateOpen, type: 'pinned_gate_open', title: `Gate aperto - ${flightNumber}`, body: `Gate aperto per il volo delle ${depTime} → ${dest}` }, - { offset: ops.gateClose, type: 'pinned_gate_close', title: `Chiusura gate - ${flightNumber}`, body: `Gate in chiusura per il volo delle ${depTime} → ${dest}` }, + // Closure phases use stdTs so they never shift when the flight is delayed. + // Only the departure notification uses etdTs (the real/estimated departure). + const phases: Array<{ offset: number; type: string; title: string; body: string; baseTs: number }> = [ + { offset: ops.checkInOpen, type: 'pinned_checkin_open', title: `Check-in aperto - ${flightNumber}`, body: `Check-in aperto per il volo delle ${depTime} → ${dest}`, baseTs: stdTs }, + { offset: ops.gateOpen, type: 'pinned_gate_open', title: `Gate aperto - ${flightNumber}`, body: `Gate aperto per il volo delle ${depTime} → ${dest}`, baseTs: stdTs }, + { offset: ops.gateClose, type: 'pinned_gate_close', title: `Chiusura gate - ${flightNumber}`, body: `Gate in chiusura per il volo delle ${depTime} → ${dest}`, baseTs: stdTs }, { offset: settings.departureLeadMinutes, type: 'pinned_departure', title: `Partenza tra ${settings.departureLeadMinutes} min - ${flightNumber}`, body: `${airline} → ${dest} · partenza alle ${depTime}`, + baseTs: etdTs, }, ]; for (const phase of phases) { - const secsUntil = ts - phase.offset * 60 - now; + const secsUntil = phase.baseTs - phase.offset * 60 - now; if (secsUntil <= 0) continue; const id = await Notifications.scheduleNotificationAsync({ content: { @@ -261,7 +265,7 @@ export async function schedulePinnedNotifications( scheduler: 'flights_pinned', type: phase.type, flightNumber, - ts, + ts: phase.baseTs, pinned: true, }), }, From 850bd9dced4a5838d3bf72547a9f80ba1ac1d075 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 20:31:25 +0000 Subject: [PATCH 2/2] chore: bump version to 2.7.29 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GwhK5jVxWTojXNS8qTQ4D3 --- android/app/build.gradle | 4 ++-- app.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bff94212..3608a19e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -105,8 +105,8 @@ android { applicationId 'com.aerostaffpro.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 104 - versionName "2.7.28" + versionCode 105 + versionName "2.7.29" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } diff --git a/app.json b/app.json index 1efe8e0d..f87eb85f 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "AeroStaff Pro", "slug": "AeroStaffPro", - "version": "2.7.28", + "version": "2.7.29", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light",