feat: application n8n Pilot complète (Expo managed workflow)

- Stack : Expo Router, Axios, Zustand, React Native Paper (thème sombre), date-fns
- Sécurité : secrets dans Android Keystore via expo-secure-store, TLS obligatoire,
  headers X-N8N-API-KEY + X-App-Token injectés par intercepteur Axios
- API : client.ts centralisé + workflows.ts + executions.ts (TypeScript strict)
- Store : Zustand appStore avec chargement depuis secure store au démarrage
- Hooks : usePolling (générique), useWorkflows, useExecutions
- Composants : StatusBadge, WorkflowCard, ExecutionCard, SkeletonLoader
- Screens : Dashboard, Workflows, Executions, Logs (détail exécution), Settings
- Navigation Expo Router : 4 tabs + stack Logs + écran Setup initial
- Docs : INSTALL.md, UPDATE.md, BACKUP.md, HAPROXY.md, SECURITY.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 17:31:55 +02:00
parent ea1705d3b0
commit 92e67d0769
41 changed files with 4891 additions and 58 deletions
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { Tabs } from 'expo-router';
import { useTheme } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
/**
* Props pour les icônes de tab — typage strict MaterialCommunityIcons.
*/
interface TabIconProps {
name: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
color: string;
size: number;
}
/** Composant d'icône de tab — évite la répétition du rendu inline */
const TabIcon: React.FC<TabIconProps> = ({ name, color, size }) => (
<MaterialCommunityIcons name={name} size={size} color={color} />
);
/**
* Layout de navigation par onglets (4 tabs principaux).
* Les logs d'exécution sont accessibles via le Stack navigator (pas de tab dédié)
* pour garder la nav propre et hiérarchiquement correcte.
*/
export default function TabsLayout() {
const theme = useTheme();
return (
<Tabs
screenOptions={{
tabBarStyle: {
backgroundColor: theme.colors.surface,
borderTopColor: theme.colors.surfaceVariant,
borderTopWidth: 1,
},
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.onSurfaceVariant,
headerStyle: { backgroundColor: theme.colors.surface },
headerTintColor: theme.colors.onSurface,
headerTitleStyle: { color: theme.colors.onSurface },
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Dashboard',
tabBarIcon: ({ color, size }) => (
<TabIcon name="view-dashboard-outline" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="workflows"
options={{
title: 'Workflows',
tabBarIcon: ({ color, size }) => (
<TabIcon name="sitemap-outline" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="executions"
options={{
title: 'Exécutions',
tabBarIcon: ({ color, size }) => (
<TabIcon name="history" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Paramètres',
tabBarIcon: ({ color, size }) => (
<TabIcon name="cog-outline" color={color} size={size} />
),
}}
/>
</Tabs>
);
}
+3
View File
@@ -0,0 +1,3 @@
import ExecutionsScreen from '../../src/screens/ExecutionsScreen';
export default ExecutionsScreen;
+3
View File
@@ -0,0 +1,3 @@
import DashboardScreen from '../../src/screens/DashboardScreen';
export default DashboardScreen;
+3
View File
@@ -0,0 +1,3 @@
import SettingsScreen from '../../src/screens/SettingsScreen';
export default SettingsScreen;
+3
View File
@@ -0,0 +1,3 @@
import WorkflowsScreen from '../../src/screens/WorkflowsScreen';
export default WorkflowsScreen;
+123
View File
@@ -0,0 +1,123 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { Stack, useRouter, useSegments } from 'expo-router';
import { PaperProvider, MD3DarkTheme, ActivityIndicator, Snackbar } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useAppStore } from '../src/store/appStore';
import { registerToastCallback } from '../src/utils/errorHandler';
/**
* Thème Material Design 3 sombre personnalisé pour n8n Pilot.
* L'orange (#FF6D3E) est la couleur d'accent principale — cohérente avec l'identité n8n.
*/
const APP_THEME = {
...MD3DarkTheme,
colors: {
...MD3DarkTheme.colors,
primary: '#FF6D3E',
primaryContainer: '#5C1C00',
secondary: '#FF9E7D',
background: '#121212',
surface: '#1E1E1E',
surfaceVariant: '#2C2C2C',
},
};
/**
* Layout racine de l'application.
* Responsabilités :
* 1. Fournit les providers globaux (GestureHandler, SafeArea, Paper)
* 2. Charge la configuration au démarrage depuis le secure store
* 3. Redirige vers /setup si aucune configuration n'existe
* 4. Enregistre le callback de toast pour l'errorHandler global
* 5. Affiche le toast d'erreur global en overlay
*/
export default function RootLayout() {
const router = useRouter();
const segments = useSegments();
const { config, loadConfig } = useAppStore();
const [isReady, setIsReady] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastVisible, setToastVisible] = useState(false);
/**
* Enregistre le callback de toast dans l'errorHandler au premier montage.
* Permet à toute la couche API de remonter des erreurs vers l'UI sans couplage direct.
*/
useEffect(() => {
registerToastCallback((message) => {
setToastMessage(message);
setToastVisible(true);
});
}, []);
/** Charge la configuration depuis le secure store puis marque l'app comme prête */
useEffect(() => {
loadConfig().finally(() => setIsReady(true));
}, []);
/**
* Gère la redirection initiale selon l'état de configuration.
* - Pas configuré et hors /setup → redirige vers /setup
* - Configuré et dans /setup → redirige vers les tabs
*/
useEffect(() => {
if (!isReady) return;
const inSetup = segments[0] === 'setup';
if (!config.isConfigured && !inSetup) {
router.replace('/setup');
} else if (config.isConfigured && inSetup) {
router.replace('/(tabs)');
}
}, [isReady, config.isConfigured, segments]);
if (!isReady) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#121212' }}>
<ActivityIndicator color="#FF6D3E" size="large" />
</View>
);
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<PaperProvider theme={APP_THEME}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="setup"
options={{
headerShown: true,
headerTitle: 'Configuration',
headerStyle: { backgroundColor: '#1E1E1E' },
headerTintColor: '#FF6D3E',
}}
/>
<Stack.Screen
name="execution/[id]"
options={{
headerShown: true,
headerTitle: "Logs d'exécution",
headerStyle: { backgroundColor: '#1E1E1E' },
headerTintColor: '#FF6D3E',
presentation: 'card',
}}
/>
</Stack>
{/* Toast global pour toutes les erreurs remontées par l'errorHandler */}
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={4000}
style={{ backgroundColor: '#4A0000' }}
>
{toastMessage}
</Snackbar>
</PaperProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
+3
View File
@@ -0,0 +1,3 @@
import LogsScreen from '../../src/screens/LogsScreen';
export default LogsScreen;
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
import SettingsScreen from '../src/screens/SettingsScreen';
/**
* Écran d'installation initiale — affiché au premier lancement.
* Partage le composant SettingsScreen avec un en-tête de bienvenue.
* Après sauvegarde, le root layout redirige automatiquement vers les tabs.
*/
export default function SetupScreen() {
const theme = useTheme();
return (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<View style={styles.header}>
<Text
variant="headlineMedium"
style={[styles.title, { color: theme.colors.primary }]}
>
n8n Pilot
</Text>
<Text
variant="bodyMedium"
style={{ color: theme.colors.onSurfaceVariant, textAlign: 'center' }}
>
Configurez votre instance n8n pour commencer
</Text>
</View>
<SettingsScreen />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: 24,
paddingTop: 24,
paddingBottom: 8,
alignItems: 'center',
gap: 4,
},
title: {
fontWeight: '700',
},
});