Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a78bc53
feat: ship in-app github updater v2.6.4
Apr 26, 2026
2747cee
Harden backup security and release signing
Apr 26, 2026
1e3c6b6
release: prepare v2.6.6
Apr 27, 2026
8804ed2
release: prepare v2.6.7
Apr 27, 2026
435a710
feat: add airport profiles
Apr 27, 2026
9c3e707
release: prepare v2.6.8
Apr 27, 2026
8566c63
feat: improve settings dialogs and airline discovery
Apr 27, 2026
03d3df9
feat: add monthly hours calendar mode
Apr 27, 2026
858a531
release: prepare v2.6.9
Apr 27, 2026
666bea5
release: prepare v2.6.10
Apr 27, 2026
899e420
release: prepare v2.6.11
Apr 28, 2026
b84eb63
release: prepare v2.6.12
Apr 28, 2026
3e92181
feat: add liquid glass surfaces to app chrome
Apr 28, 2026
b8beaf7
release: prepare v2.6.13
Apr 28, 2026
7d4cc41
fix: disable native liquid glass runtime
Apr 28, 2026
614f64f
fix: harden native liquid glass runtime
Apr 28, 2026
b3dd0d4
release: prepare v2.6.15
Apr 28, 2026
f05965c
fix: gate liquid glass before react host init
Apr 28, 2026
b98e434
release: prepare v2.6.16
Apr 28, 2026
8132b57
release: prepare v2.6.17
Apr 28, 2026
7dde3dd
release: prepare v2.6.18
Apr 28, 2026
b082d82
release: prepare v2.6.19
Apr 28, 2026
570d564
release: prepare v2.6.20
Apr 28, 2026
ff118cc
Upgrade liquid glass to 1.0.4 on compileSdk 37
Apr 28, 2026
5e161eb
Add Android exit diagnostics for liquid glass crashes
Apr 28, 2026
6309d99
Capture native exit traces for liquid glass recovery
Apr 28, 2026
7668750
Parse native tombstone traces in runtime diagnostics
Apr 28, 2026
2b4f30f
release: prepare v2.6.25 notifications settings
Apr 29, 2026
517211e
release: prepare v2.6.26 local notification and monitor updates
Apr 29, 2026
71daf03
release: remove liquid glass and prepare v2.6.27
Apr 29, 2026
7c9e61b
Improve menu readability and airline picker branding; release v2.6.28
Apr 29, 2026
527d330
Fix flight notification settings entry, clean airline picker, and rel…
Apr 29, 2026
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
56 changes: 22 additions & 34 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
ref:
description: 'Branch o commit da buildare'
required: false
default: 'claude/fix-shift-permissions-Cn8gk'
default: 'main'
version_override:
description: 'Tag release (es. v2.6.2). Vuoto = legge app.json'
required: false
Expand All @@ -28,6 +28,11 @@ env:
jobs:
build:
runs-on: ubuntu-latest
env:
RELEASE_STORE_FILE: ${{ github.workspace }}/android/app/release.keystore
RELEASE_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
RELEASE_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

steps:
- name: Compute checkout ref
Expand All @@ -38,7 +43,7 @@ jobs:
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
echo "ref=claude/fix-shift-permissions-Cn8gk" >> "$GITHUB_OUTPUT"
echo "ref=main" >> "$GITHUB_OUTPUT"
fi

- name: Checkout
Expand Down Expand Up @@ -74,8 +79,8 @@ jobs:
run: |
echo "y" | sdkmanager \
"ndk;27.1.12297006" \
"build-tools;36.0.0" \
"platforms;android-36" \
"build-tools;37.0.0" \
"platforms;android-37.0" \
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use valid Android platform package path in sdkmanager

The workflow installs "platforms;android-37.0", but sdkmanager expects SDK-style package IDs like "platforms;android-36" (Android docs show this exact format). Using the .0 suffix makes the package path invalid, so CI release builds can fail before Gradle runs when this step executes.

Useful? React with 👍 / 👎.

"cmake;3.22.1"

- name: Install dependencies
Expand Down Expand Up @@ -104,10 +109,20 @@ jobs:
- name: Make gradlew executable
run: chmod +x android/gradlew

- name: Restore stable keystore
- name: Restore release keystore
run: |
if [[ -z "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
echo "Missing KEYSTORE_BASE64 secret"
exit 1
fi
for var in RELEASE_STORE_PASSWORD RELEASE_KEY_ALIAS RELEASE_KEY_PASSWORD; do
if [[ -z "${!var}" ]]; then
echo "Missing $var secret"
exit 1
fi
done
mkdir -p android/app
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/debug.keystore
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RELEASE_STORE_FILE"

- name: Build release APK
run: |
Expand All @@ -131,34 +146,7 @@ jobs:
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: "AeroStaff Pro ${{ steps.meta.outputs.tag }}"
body: |
## 📦 AeroStaff Pro ${{ steps.meta.outputs.tag }}

### Novità
- **Backup / Ripristino**: esporta tutti i dati dell'app in un file JSON e reimportali in qualsiasi momento da Impostazioni
- **Aggiornamento in-app**: popup stile Mihon con changelog e download APK diretto; controllo automatico all'avvio ogni 24h
- **Impostazioni riprogettate**: sezione aggiornamenti con card dedicata e badge NEW; backup con due tile colorate (esporta verde, importa blu)

### Bug fix – Turni e Home
- **HomeScreen "Giorno di riposo" errato**: l'evento Lavoro nel calendario ora ha sempre la precedenza su un eventuale evento Riposo dello stesso giorno
- **Widget "Giorno di riposo" errato**: WIDGET_SHIFT_KEY è ora la fonte autoritativa per classificare lavoro/riposo, impedendo che dati in cache stantii sovrascrivano il turno reale
- **FlightScreen**: trovato evento Lavoro nel calendario non azzerava `isRestDay` se preceduto da un evento Riposo; corretto

### Bug fix – Voli
- **Aggiornamento automatico voli**: i dati FR24 ora si ricaricano ogni 2 minuti senza riaprire l'app
- **Duplicati voli**: chiave di merge stabilizzata su `numeroVolo_tsPartenza` (prima usava `identification.id` che FR24 a volte omette)

### Bug fix – StaffMonitor stand / gate / arrivi
- **Colonne non rilevate**: rimosso il requisito di trovare stand/gate/belt prima di parsare; prima saltava tutte le righe
- **Nomi handler nella colonna stand**: `cell()` ora estrae solo il primo token alfanumerico (es. `17◆ Federico` → `17`)
- **Numeri di telefono come stand**: `isPhoneOrJunk` individua sequenze di 8+ cifre ovunque nella stringa
- **Colonna "ADDETTO STAND"**: word-boundary `\bstand\b` per evitare falsi positivi
- **Arrivi in parallelo + cache**: 7 varianti URL `nature=A` in simultanea, prima risposta vince in ≤40s; cache AsyncStorage 20 min
- **Sessione Tomcat (JSESSIONID)**: cookie catturato dalla risposta D e inoltrato alle richieste A — fix AbortError sugli arrivi
- **Numeri volo corrotti**: rimossi URL fallback senza `nature=A`
- **Timeout partenze**: ripristinato a 25s

Scarica `AeroStaffPro-${{ steps.meta.outputs.tag }}.apk` e installalo sul tuo dispositivo Android.
generate_release_notes: true
files: ${{ env.apk }}
draft: false
prerelease: false
84 changes: 61 additions & 23 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ import PhonebookScreen from './src/screens/PhonebookScreen';
import SettingsScreen from './src/screens/SettingsScreen';
import PasswordScreen from './src/screens/PasswordScreen';
import DrawerMenu from './src/components/DrawerMenu';
import ProfileSwitcherModal from './src/components/ProfileSwitcherModal';
import FrostedSurface from './src/components/FrostedSurface';
import {
installGlobalCrashHandler,
markRuntimeStartupCompleted,
} from './src/utils/runtimeDiagnostics';
import { autoScheduleNotifications } from './src/utils/autoNotifications';
import { checkForUpdate, wasUpdateSeen, markUpdateSeen, type UpdateInfo } from './src/utils/updateChecker';
import UpdateModal from './src/components/UpdateModal';
import { useAirport } from './src/context/AirportContext';

installGlobalCrashHandler();

type Tab = 'Shifts' | 'Calendar' | 'Flights' | 'TravelDoc';
type OverlayScreen = 'Notepad' | 'Phonebook' | 'Passwords' | 'Manuals' | 'Settings' | null;
Expand Down Expand Up @@ -50,13 +59,13 @@ function GlassTab({ icon, label, focused, activeColor, inactiveColor, onPress }:
}) {
const scale = useRef(new Animated.Value(focused ? 1.15 : 1)).current;
const translateY = useRef(new Animated.Value(focused ? -4 : 0)).current;
const opacity = useRef(new Animated.Value(focused ? 1 : 0.6)).current;
const opacity = useRef(new Animated.Value(focused ? 1 : 0.78)).current;

useEffect(() => {
Animated.parallel([
Animated.spring(scale, { toValue: focused ? 1.15 : 1, useNativeDriver: true, tension: 200, friction: 15 }),
Animated.spring(translateY, { toValue: focused ? -4 : 0, useNativeDriver: true, tension: 200, friction: 15 }),
Animated.timing(opacity, { toValue: focused ? 1 : 0.5, duration: 150, useNativeDriver: true }),
Animated.timing(opacity, { toValue: focused ? 1 : 0.74, duration: 150, useNativeDriver: true }),
]).start();
}, [focused]);

Expand All @@ -81,10 +90,13 @@ function GlassTab({ icon, label, focused, activeColor, inactiveColor, onPress }:
function AppInner() {
const { colors, mode } = useAppTheme();
const { t } = useLanguage();
const { profileInitials } = useAirport();
const [activeTab, setActiveTab] = useState<Tab>('Shifts');
const [drawerOpen, setDrawerOpen] = useState(false);
const [overlay, setOverlay] = useState<OverlayScreen>(null);
const [openFlightNotifSettingsSignal, setOpenFlightNotifSettingsSignal] = useState(0);
const [pendingUpdate, setPendingUpdate] = useState<UpdateInfo | null>(null);
const [profileModalOpen, setProfileModalOpen] = useState(false);

const tabLabels: Record<Tab, string> = {
Shifts: t('tabHome'), Calendar: t('tabShifts'), Flights: t('tabFlights'), TravelDoc: t('tabTravelDoc'),
Expand All @@ -99,6 +111,8 @@ function AppInner() {

// ─── Auto-schedule flight notifications on startup ─────────────────────────
useEffect(() => {
markRuntimeStartupCompleted().catch(() => {});

autoScheduleNotifications().then(count => {
if (count > 0 && __DEV__) console.log(`Auto-scheduled ${count} notifications`);
}).catch(() => {});
Expand All @@ -108,6 +122,7 @@ function AppInner() {
const seen = await wasUpdateSeen(info.latestVersion);
if (!seen) setPendingUpdate(info);
}).catch(() => {});

}, []);

// ─── Android back button: overlay → home, drawer → close ───────────────────
Expand All @@ -133,6 +148,13 @@ function AppInner() {
setActiveTab(TABS[newIdx].id);
};

const openFlightNotificationsFromSettings = () => {
setDrawerOpen(false);
setOverlay(null);
goToTab(2);
setOpenFlightNotifSettingsSignal(prev => prev + 1);
};

const swipePan = useMemo(() => PanResponder.create({
onMoveShouldSetPanResponder: (_, g) =>
Math.abs(g.dx) > 30 && Math.abs(g.dx) > Math.abs(g.dy) * 2,
Expand Down Expand Up @@ -170,22 +192,23 @@ function AppInner() {
if (overlay === 'Phonebook') return <PhonebookScreen />;
if (overlay === 'Passwords') return <PasswordScreen />;
if (overlay === 'Manuals') return <ManualsScreen />;
if (overlay === 'Settings') return <SettingsScreen />;
if (overlay === 'Settings') return <SettingsScreen onOpenFlightNotifications={openFlightNotificationsFromSettings} />;
return null;
};

const renderTabScreen = (tab: Tab) => {
switch (tab) {
case 'Shifts': return <HomeScreen isFocused={activeTab === 'Shifts'} />;
case 'Calendar': return <CalendarScreen />;
case 'Flights': return <FlightScreen />;
case 'Flights': return <FlightScreen openNotifSettingsSignal={openFlightNotifSettingsSignal} />;
case 'TravelDoc': return <TraveldocScreen />;
}
};


const appBarTitle = overlay ? overlayTitles[overlay] : 'AeroStaff Pro';
const isWeather = mode === 'weather' && !!colors.gradient;
const tabInactiveColor = colors.isDark ? 'rgba(235,239,245,0.78)' : colors.tabIconInactive;

return (
<View style={[styles.root, { backgroundColor: colors.bg, paddingTop: StatusBar.currentHeight || 48 }]}>
Expand All @@ -194,7 +217,7 @@ function AppInner() {
backgroundColor={colors.appBar}
/>

{/* Top App Bar — liquid glass */}
{/* Top App Bar */}
<ExpoBlurView
intensity={colors.isDark ? 60 : 50}
tint={colors.isDark ? 'dark' : 'light'}
Expand All @@ -216,14 +239,16 @@ function AppInner() {
<Text style={styles.weatherChip}>{colors.weatherIcon} {colors.weatherLabel}</Text>
)}
</View>
<LinearGradient
colors={[colors.primaryLight, colors.primary]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.avatar}
>
<Text style={styles.avatarText}>MR</Text>
</LinearGradient>
<TouchableOpacity onPress={() => setProfileModalOpen(true)} activeOpacity={0.85}>
<LinearGradient
colors={[colors.primaryLight, colors.primary]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.avatar}
>
<Text style={styles.avatarText}>{profileInitials}</Text>
</LinearGradient>
</TouchableOpacity>
</ExpoBlurView>

{/* Screen Content */}
Expand Down Expand Up @@ -257,12 +282,16 @@ function AppInner() {
{/* Bottom Nav — Glassmorphic Floating Pill (hidden on overlay screens) */}
{!overlay && (
<View style={styles.tabBarWrapper} {...swipePan.panHandlers}>
<View style={[styles.tabBarBlur, { backgroundColor: colors.isDark ? 'rgba(28,28,30,0.82)' : 'rgba(242,242,247,0.82)' }]}>
<ExpoBlurView
intensity={80}
tint={colors.isDark ? 'dark' : 'light'}
style={StyleSheet.absoluteFill}
/>
<FrostedSurface
style={styles.tabBarBlur}
blurIntensity={90}
blurTint={colors.isDark ? 'dark' : 'light'}
baseColor={colors.isDark ? 'rgba(8,11,16,0.84)' : 'rgba(248,250,255,0.88)'}
gradientColors={colors.isDark
? ['rgba(255,255,255,0.05)', 'rgba(9,11,15,0.66)']
: ['rgba(255,255,255,0.55)', 'rgba(255,244,230,0.34)']}
overlayColor={colors.isDark ? 'rgba(0,0,0,0.40)' : 'rgba(255,255,255,0.10)'}
>
<View style={styles.tabBarRow}>
{TABS.map(tab => {
const active = activeTab === tab.id;
Expand All @@ -273,7 +302,7 @@ function AppInner() {
label={tabLabels[tab.id]}
focused={active}
activeColor={colors.tabIconActive}
inactiveColor={colors.tabIconInactive}
inactiveColor={tabInactiveColor}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
goToTab(TABS.findIndex(t => t.id === tab.id));
Expand All @@ -282,7 +311,7 @@ function AppInner() {
);
})}
</View>
</View>
</FrostedSurface>
</View>
)}

Expand All @@ -292,6 +321,10 @@ function AppInner() {
onClose={() => setDrawerOpen(false)}
onSelect={handleDrawerSelect}
/>
<ProfileSwitcherModal
visible={profileModalOpen}
onClose={() => setProfileModalOpen(false)}
/>
{pendingUpdate && (
<UpdateModal
info={pendingUpdate}
Expand Down Expand Up @@ -358,8 +391,13 @@ const styles = StyleSheet.create({
height: 66,
borderRadius: 33,
overflow: 'hidden',
borderWidth: 0.75,
borderColor: 'rgba(255,255,255,0.22)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.28)',
shadowColor: '#000',
shadowOpacity: 0.24,
shadowRadius: 14,
shadowOffset: { width: 0, height: 4 },
elevation: 12,
},
tabBarRow: {
flex: 1,
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,30 @@ npm run typecheck

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

Latest stable: **v1.3.2**
Latest stable: **v2.6.29**

To install:

1. Open the Releases section and download `FlightWorkApp-vX.X.X.apk`.
1. Open the Releases section and download `AeroStaffPro-vX.X.X.apk`.
2. Transfer to your Android device and install (enable "Unknown sources" if needed).
3. For Wear OS, pair the phone app — the watch companion installs automatically.

To build locally:

```bash
$env:RELEASE_STORE_FILE="C:\path\to\release.keystore"
$env:RELEASE_STORE_PASSWORD="your-keystore-password"
$env:RELEASE_KEY_ALIAS="your-key-alias"
$env:RELEASE_KEY_PASSWORD="your-key-password"

cd android
.\gradlew.bat assembleRelease
# Output: android/app/build/outputs/apk/release/app-release.apk
```

The GitHub Actions release workflow expects these repository secrets:
`KEYSTORE_BASE64`, `KEYSTORE_PASSWORD`, `KEY_ALIAS`, and `KEY_PASSWORD`.

## Branch Structure

- `main`: most stable and shareable branch
Expand Down
25 changes: 25 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Security Policy

## Supported Versions

| Version | Supported |
| ------- | --------- |
| 2.x | Yes |
| < 2.0 | No |

## Reporting a Vulnerability

Please do not open public GitHub issues for suspected security vulnerabilities.

Preferred channel:

- Use GitHub Private Vulnerability Reporting for this repository when available.

If a private reporting channel is temporarily unavailable:

- Open a minimal public issue without exploit details and ask the maintainer to continue privately.
- Include the affected version, impact, reproduction steps, and any suggested mitigation.

## Response Expectations

The project aims to acknowledge new reports within 7 days and share follow-up status until the issue is resolved or triaged.
Loading
Loading