Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ jobs:
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
GRADLE_OPTS: "-Xmx4g -XX:MaxMetaspaceSize=512m"

- name: Validate APK release metadata
run: |
APK=$(find android/app/build/outputs/apk/release -name "*.apk" -type f | head -1)
BADGING=$("$ANDROID_HOME/build-tools/37.0.0/aapt" dump badging "$APK")
APK_VERSION_NAME=$(echo "$BADGING" | sed -n "s/.*versionName='\([^']*\)'.*/\1/p")
APK_VERSION_CODE=$(echo "$BADGING" | sed -n "s/.*versionCode='\([^']*\)'.*/\1/p")
EXPECTED_TAG="v${APK_VERSION_NAME}"

echo "APK versionName=$APK_VERSION_NAME"
echo "APK versionCode=$APK_VERSION_CODE"
echo "Release tag=${{ steps.meta.outputs.tag }}"

if [[ "${{ steps.meta.outputs.version }}" != "$APK_VERSION_NAME" ]]; then
echo "app.json version (${{ steps.meta.outputs.version }}) does not match APK versionName ($APK_VERSION_NAME)"
exit 1
fi

if [[ "${{ steps.meta.outputs.tag }}" != "$EXPECTED_TAG" ]]; then
echo "Release tag (${{ steps.meta.outputs.tag }}) does not match APK versionName ($APK_VERSION_NAME)"
exit 1
fi

- name: Rename APK
run: |
APK=$(find android/app/build/outputs/apk/release -name "*.apk" -type f | head -1)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ npm run typecheck

APK files are published in [GitHub Releases](https://github.com/TargetMisser/FlightWorkApp/releases).

Latest stable: **v2.6.29**
Latest stable: **v2.6.36**

To install:

Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ android {
applicationId 'com.aerostaffpro.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 38
versionName "2.6.29"
versionCode 42
versionName "2.6.36"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "AeroStaff Pro",
"slug": "AeroStaffPro",
"version": "2.6.29",
"version": "2.6.36",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aerostaff-pro",
"version": "2.6.29",
"version": "2.6.36",
"main": "index.ts",
"scripts": {
"start": "expo start",
Expand Down
30 changes: 28 additions & 2 deletions src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const it = {
overlayNotepad: 'Blocco Note', overlayPhonebook: 'Rubrica', overlayPasswords: 'Password',
overlayManuals: 'Manuali DCS', overlaySettings: 'Impostazioni',
// Common
cancel: 'Annulla', save: 'Salva', delete: 'Elimina', error: 'Errore',
cancel: 'Annulla', save: 'Salva', delete: 'Elimina', remove: 'Rimuovi', error: 'Errore',
confirm: 'Conferma', ok: 'OK', add: 'Aggiungi',
profileTitle: 'Profili aeroporto',
profileSubtitle: 'Salva aeroporto e compagnie diverse, poi cambia profilo con un tap.',
Expand Down Expand Up @@ -42,6 +42,17 @@ const it = {
sectionAirport: 'AEROPORTO',
airportBase: 'Aeroporto base', airportLoading: 'Caricamento aeroporto...',
airportAirlines: 'Compagnie monitorate', airportAirlinesSub: 'Wizz, easyJet, Ryanair…',
sectionFlightData: 'DATI VOLI',
airLabsKey: 'AirLabs sperimentale',
airLabsKeyNotConfigured: 'Non configurato · usa StaffMonitor come fallback',
airLabsKeyDevice: 'Configurato sul dispositivo',
airLabsKeyBuild: 'Configurato nella build',
airLabsModalTitle: 'Chiave API AirLabs',
airLabsModalCopy: 'Incolla una chiave AirLabs per provarlo come fonte voli. Resta nel SecureStore del telefono; se non va, StaffMonitor resta il fallback.',
airLabsModalLabel: 'API key',
airLabsKeySavedTitle: 'AirLabs configurato',
airLabsKeySavedMsg: 'Aggiorna la tab Voli per provare AirLabs come fonte dati.',
airLabsKeyErrorMsg: 'Non sono riuscito a salvare la chiave AirLabs.',
sectionNotifications: 'NOTIFICHE',
notifFlights: 'Notifiche voli', notifFlightsSub: 'Gestito nella tab Voli',
notifReminder: 'Promemoria turno', notifReminderSub: 'Prossimamente',
Expand Down Expand Up @@ -118,6 +129,8 @@ const it = {
flightTitle: 'Voli in tempo reale', flightArrivals: 'Arrivi', flightDepartures: 'Partenze',
flightToday: 'Oggi', flightTomorrow: 'Domani',
flightNoFlights: 'Nessun volo per questo giorno.',
flightDataSource: 'Fonte voli',
flightProviderDiagnosticsTitle: 'Diagnostica voli',
flightCheckin: 'Check-in', flightGate: 'Gate', flightStand: 'Stand', flightBelt: 'Nastro', flightDeparted: 'Partito',
flightLanded: 'Atterrato', flightEstimated: 'Stimato', flightOnTime: 'In orario',
flightPinned: 'PINNATO', flightPinnedLabel: 'Pinnato',
Expand Down Expand Up @@ -206,7 +219,7 @@ const en: typeof it = {
overlayNotepad: 'Notepad', overlayPhonebook: 'Phonebook', overlayPasswords: 'Password',
overlayManuals: 'DCS Manuals', overlaySettings: 'Settings',
// Common
cancel: 'Cancel', save: 'Save', delete: 'Delete', error: 'Error',
cancel: 'Cancel', save: 'Save', delete: 'Delete', remove: 'Remove', error: 'Error',
confirm: 'Confirm', ok: 'OK', add: 'Add',
profileTitle: 'Airport profiles',
profileSubtitle: 'Save different airports and airline sets, then switch profiles with one tap.',
Expand Down Expand Up @@ -241,6 +254,17 @@ const en: typeof it = {
sectionAirport: 'AIRPORT',
airportBase: 'Home airport', airportLoading: 'Loading airport...',
airportAirlines: 'Monitored airlines', airportAirlinesSub: 'Wizz, easyJet, Ryanair…',
sectionFlightData: 'FLIGHT DATA',
airLabsKey: 'Experimental AirLabs',
airLabsKeyNotConfigured: 'Not configured · using StaffMonitor fallback',
airLabsKeyDevice: 'Configured on device',
airLabsKeyBuild: 'Configured in build',
airLabsModalTitle: 'AirLabs API key',
airLabsModalCopy: 'Paste an AirLabs key to test it as a flight source. It stays in the phone SecureStore; if it fails, StaffMonitor remains the fallback.',
airLabsModalLabel: 'API key',
airLabsKeySavedTitle: 'AirLabs configured',
airLabsKeySavedMsg: 'Refresh the Flights tab to test AirLabs as data source.',
airLabsKeyErrorMsg: 'Could not save the AirLabs key.',
sectionNotifications: 'NOTIFICATIONS',
notifFlights: 'Flight notifications', notifFlightsSub: 'Managed in Flights tab',
notifReminder: 'Shift reminder', notifReminderSub: 'Coming soon',
Expand Down Expand Up @@ -317,6 +341,8 @@ const en: typeof it = {
flightTitle: 'Real-time Flights', flightArrivals: 'Arrivals', flightDepartures: 'Departures',
flightToday: 'Today', flightTomorrow: 'Tomorrow',
flightNoFlights: 'No flights for this day.',
flightDataSource: 'Flight source',
flightProviderDiagnosticsTitle: 'Flight diagnostics',
flightCheckin: 'Check-in', flightGate: 'Gate', flightStand: 'Stand', flightBelt: 'Belt', flightDeparted: 'Departed',
flightLanded: 'Landed', flightEstimated: 'Estimated', flightOnTime: 'On time',
flightPinned: 'PINNED', flightPinnedLabel: 'Pinned',
Expand Down
108 changes: 101 additions & 7 deletions src/screens/FlightScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useAppTheme, type ThemeColors } from '../context/ThemeContext';
import { useAirport } from '../context/AirportContext';
import { getAirlineOps, getAirlineColor, AIRLINE_COLORS, AIRLINE_DISPLAY_NAMES } from '../utils/airlineOps';
import { fetchAirportScheduleRaw } from '../utils/fr24api';
import { fetchAirportScheduleRaw, type FlightScheduleProviderStatus } from '../utils/fr24api';
import { fetchStaffMonitorData, normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor';
import { formatAirportHeader, getAirportAirlines, getStoredAirportAirlines } from '../utils/airportSettings';
import { requestWidgetUpdate } from 'react-native-android-widget';
Expand All @@ -36,6 +36,11 @@ const FLIGHTS_RETENTION_SECONDS = 60 * 60;
const MIN_NOTIF_MINUTES = 1;
const MAX_NOTIF_MINUTES = 90;
type FlightAlertTone = 'success' | 'warning' | 'info';
type FlightDataSourceState = {
sourceLabel: string;
fetchedAt: number;
diagnostics: FlightScheduleProviderStatus[];
};

type FlightNotificationSettings = {
onlyTrackedAirlines: boolean;
Expand All @@ -57,6 +62,39 @@ const DEFAULT_NOTIFICATION_SETTINGS: FlightNotificationSettings = {
departureLeadMinutes: 10,
};

function formatFlightProviderDiagnostics(
source: FlightDataSourceState | null,
locale: string,
): string {
if (!source) return '';

const fetchedLabel = new Date(source.fetchedAt).toLocaleTimeString(locale, {
hour: '2-digit',
minute: '2-digit',
});
const isItalian = locale.startsWith('it');
const lines = [
`${isItalian ? 'Fonte attiva' : 'Active source'}: ${source.sourceLabel}`,
`${isItalian ? 'Aggiornato' : 'Updated'}: ${fetchedLabel}`,
];

for (const item of source.diagnostics) {
const status = item.status === 'success'
? 'OK'
: item.status === 'skipped'
? (isItalian ? 'saltato' : 'skipped')
: (isItalian ? 'errore' : 'error');
const counts = item.status === 'success'
? ` · A:${item.arrivals ?? 0} D:${item.departures ?? 0}`
: '';
const timing = typeof item.durationMs === 'number' ? ` · ${item.durationMs}ms` : '';
const message = item.message ? ` · ${item.message}` : '';
lines.push(`${item.label}: ${status}${counts}${timing}${message}`);
}

return lines.join('\n');
}

function normalizeAirlineKey(value: unknown): string {
return typeof value === 'string'
? value.trim().toLowerCase().replace(/\s+/g, ' ')
Expand Down Expand Up @@ -126,7 +164,9 @@ function mergeFlights(cached: any[], fresh: any[], tsField: string): any[] {
function pruneExpiredFlights(items: any[], tsField: string, nowSeconds = Date.now() / 1000): any[] {
const cutoff = nowSeconds - FLIGHTS_RETENTION_SECONDS;
return items.filter(item => {
const ts = item.flight?.time?.scheduled?.[tsField];
const ts = item.flight?.time?.real?.[tsField]
|| item.flight?.time?.estimated?.[tsField]
|| item.flight?.time?.scheduled?.[tsField];
if (!ts) return true;
return ts >= cutoff;
});
Expand Down Expand Up @@ -430,6 +470,15 @@ function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin,
const smFlight =
smPool.find(sm => sm.flightNumber === normFn) ??
smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped);
const operational = item.flight?._operational ?? {};
const terminalGate = (terminal?: string, gate?: string) => {
if (terminal && gate) return `${terminal}/${gate}`;
return gate ?? terminal ?? '—';
};
const standLabel = smFlight?.stand ?? operational.stand ?? '—';
const checkinLabel = smFlight?.checkin ?? operational.checkin ?? '—';
const gateLabel = smFlight?.gate ?? terminalGate(operational.departureTerminal, operational.departureGate);
const beltLabel = smFlight?.belt ?? operational.belt ?? '—';

const arrivalProgress = activeTab === 'arrivals' && ts ? (() => {
const scheduledDep = item.flight?.time?.scheduled?.departure;
Expand Down Expand Up @@ -661,23 +710,23 @@ function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin,
<View style={s.smFooter}>
<View style={s.smPill}>
<MaterialIcons name="local-parking" size={11} color={colors.primary} />
<Text style={s.smPillText}>Stand {smFlight?.stand ?? '—'}</Text>
<Text style={s.smPillText}>Stand {standLabel}</Text>
</View>
{activeTab === 'departures' ? (
<>
<View style={s.smPill}>
<MaterialIcons name="desktop-windows" size={11} color={colors.primary} />
<Text style={s.smPillText}>{t('flightCheckin')} {smFlight?.checkin ?? '—'}</Text>
<Text style={s.smPillText}>{t('flightCheckin')} {checkinLabel}</Text>
</View>
<View style={s.smPill}>
<MaterialIcons name="meeting-room" size={11} color={colors.primary} />
<Text style={s.smPillText}>{t('flightGate')} {smFlight?.gate ?? '—'}</Text>
<Text style={s.smPillText}>{t('flightGate')} {gateLabel}</Text>
</View>
</>
) : (
<View style={s.smPill}>
<MaterialIcons name="luggage" size={11} color={colors.primary} />
<Text style={s.smPillText}>{t('flightBelt')} {smFlight?.belt ?? '—'}</Text>
<Text style={s.smPillText}>{t('flightBelt')} {beltLabel}</Text>
</View>
)}
</View>
Expand Down Expand Up @@ -913,6 +962,7 @@ export default function FlightScreen({ openNotifSettingsSignal = 0 }: FlightScre
const [staffMonitorDeps, setStaffMonitorDeps] = useState<StaffMonitorFlight[]>([]);
const [staffMonitorArrs, setStaffMonitorArrs] = useState<StaffMonitorFlight[]>([]);
const [notifSettings, setNotifSettings] = useState<FlightNotificationSettings>(DEFAULT_NOTIFICATION_SETTINGS);
const [flightDataSource, setFlightDataSource] = useState<FlightDataSourceState | null>(null);
const lastOpenNotifSettingsSignalRef = useRef(openNotifSettingsSignal);
const applySelectedAirlines = useCallback((next: string[]) => {
setSelectedAirlines(next);
Expand Down Expand Up @@ -961,6 +1011,13 @@ export default function FlightScreen({ openNotifSettingsSignal = 0 }: FlightScre
if (cache.date === today) {
setAllArrivalsFull(cache.arrivals ?? []);
setAllDeparturesFull(cache.departures ?? []);
if (cache.sourceLabel && cache.fetchedAt) {
setFlightDataSource({
sourceLabel: cache.sourceLabel,
fetchedAt: cache.fetchedAt,
diagnostics: Array.isArray(cache.providerDiagnostics) ? cache.providerDiagnostics : [],
});
}
}
} catch {}
});
Expand Down Expand Up @@ -1016,6 +1073,9 @@ export default function FlightScreen({ openNotifSettingsSignal = 0 }: FlightScre
allDepartures,
departures: fetchedDepartures,
arrivals: fetchedArrivals,
sourceLabel,
providerDiagnostics,
fetchedAt,
} = await fetchAirportScheduleRaw(airportCode);
const nextAirportAirlines = getAirportAirlines(airportCode);
setAirportAirlines(nextAirportAirlines);
Expand Down Expand Up @@ -1047,9 +1107,23 @@ export default function FlightScreen({ openNotifSettingsSignal = 0 }: FlightScre
} catch {}
const mergedArrs = pruneExpiredFlights(mergeFlights(cachedArrs, allArrivals, 'arrival'), 'arrival');
const mergedDeps = pruneExpiredFlights(mergeFlights(cachedDeps, allDepartures, 'departure'), 'departure');
const sourceState: FlightDataSourceState = {
sourceLabel: sourceLabel ?? 'Sconosciuta',
fetchedAt: fetchedAt ?? Date.now(),
diagnostics: providerDiagnostics ?? [],
};
setAllArrivalsFull(mergedArrs);
setAllDeparturesFull(mergedDeps);
AsyncStorage.setItem(FLIGHTS_CACHE_KEY, JSON.stringify({ arrivals: mergedArrs, departures: mergedDeps })).catch(() => {});
setFlightDataSource(sourceState);
AsyncStorage.setItem(FLIGHTS_CACHE_KEY, JSON.stringify({
date: new Date().toISOString().split('T')[0],
airportCode,
arrivals: mergedArrs,
departures: mergedDeps,
sourceLabel: sourceState.sourceLabel,
fetchedAt: sourceState.fetchedAt,
providerDiagnostics: sourceState.diagnostics,
})).catch(() => {});

// Build inbound arrival map: registration → best known arrival timestamp
const inboundMap: Record<string, number> = {};
Expand Down Expand Up @@ -1540,6 +1614,24 @@ export default function FlightScreen({ openNotifSettingsSignal = 0 }: FlightScre
</View>
</View>

{flightDataSource && (
<TouchableOpacity
style={s.sourceBadge}
activeOpacity={0.78}
onPress={() => setNotifDialog({
title: t('flightProviderDiagnosticsTitle'),
message: formatFlightProviderDiagnostics(flightDataSource, locale),
tone: 'info',
})}
>
<MaterialIcons name="hub" size={14} color={colors.primary} />
<Text style={s.sourceBadgeText}>
{t('flightDataSource')}: {flightDataSource.sourceLabel}
</Text>
<MaterialIcons name="info-outline" size={14} color={colors.textSub} />
</TouchableOpacity>
)}

{loading ? (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color={colors.primary} />
Expand Down Expand Up @@ -1840,6 +1932,8 @@ function makeStyles(c: ThemeColors) {
pageTitle: { fontSize: 22, fontWeight: 'bold', color: c.primaryDark },
pageSub: { fontSize: 13, color: c.textSub, marginTop: 2 },
controlsRow: { flexDirection: 'row', gap: 8, padding: 12, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border },
sourceBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', marginTop: 10, marginHorizontal: 16, paddingHorizontal: 10, paddingVertical: 7, borderRadius: 999, backgroundColor: c.primaryLight, borderWidth: 1, borderColor: c.glassBorder },
sourceBadgeText: { fontSize: 11, fontWeight: '800', color: c.primaryDark },
segment: { flex: 1, flexDirection: 'row', backgroundColor: c.bg, borderRadius: 8, padding: 3 },
segBtn: { flex: 1, paddingVertical: 7, alignItems: 'center', borderRadius: 6 },
segBtnActive: { backgroundColor: c.card, borderWidth: 1, borderColor: c.primaryLight },
Expand Down
Loading
Loading