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
+91
View File
@@ -0,0 +1,91 @@
import React, { useEffect, useRef } from 'react';
import { View, Animated, StyleSheet, ViewStyle } from 'react-native';
interface SkeletonItemProps {
width?: number | `${number}%`;
height?: number;
borderRadius?: number;
style?: ViewStyle;
}
/**
* Bloc de squelette animé avec effet de pulsation (shimmer).
* Composant interne utilisé par SkeletonLoader pour construire les cartes placeholder.
*/
const SkeletonItem: React.FC<SkeletonItemProps> = ({
width = '100%',
height = 16,
borderRadius = 4,
style,
}) => {
const opacity = useRef(new Animated.Value(0.3)).current;
useEffect(() => {
/** Boucle d'animation : alterne entre 30% et 70% d'opacité */
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 0.7,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [opacity]);
return (
<Animated.View
style={[
styles.skeleton,
{ width: width as number, height, borderRadius, opacity },
style,
]}
/>
);
};
interface SkeletonLoaderProps {
/** Nombre de cartes placeholder à afficher pendant le chargement initial */
count?: number;
}
/**
* Affiche des cartes placeholder animées pendant le chargement des données.
* Simule la structure d'une WorkflowCard/ExecutionCard pour éviter le layout shift
* et indiquer à l'utilisateur que du contenu arrive.
*/
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({ count = 5 }) => {
return (
<>
{Array.from({ length: count }).map((_, index) => (
<View key={index} style={styles.card}>
<SkeletonItem width="60%" height={18} style={{ marginBottom: 8 }} />
<SkeletonItem width="85%" height={12} style={{ marginBottom: 6 }} />
<SkeletonItem width="40%" height={12} />
</View>
))}
</>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: '#1E1E1E',
marginHorizontal: 16,
marginVertical: 6,
padding: 16,
borderRadius: 12,
},
skeleton: {
backgroundColor: '#2C2C2C',
},
});
export default SkeletonLoader;