diff --git a/src/components/ServiceHealthIndicator.js b/src/components/ServiceHealthIndicator.js new file mode 100644 index 0000000..34ed1ce --- /dev/null +++ b/src/components/ServiceHealthIndicator.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; + +/** + * Circular health indicator dot + * + * status values: + * 'inactive' → grey (not used / not connected) + * 'healthy' → green (connected and working) + * 'error' → red (connected but errors fetching data) + * 'checking' → amber (currently checking) + */ +const COLORS = { + inactive: '#9e9e9e', + healthy: '#34a853', + error: '#d93025', + checking: '#fbbc04', +}; + +const ServiceHealthIndicator = ({ status = 'inactive', size = 10 }) => ( + +); + +const styles = StyleSheet.create({ + dot: { + marginRight: 8, + }, +}); + +export default ServiceHealthIndicator; + diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index 171df85..2a0817c 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService'; import GoogleAUTH from '../AUTH/GoogleAUTH'; import NASAUTH from '../AUTH/NASAUTH'; import NASSetupInstructions from '../components/NASSetupInstructions'; +import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; const OnboardingScreen = ({ navigation, route }) => { const { userId } = route.params; @@ -25,6 +26,11 @@ const OnboardingScreen = ({ navigation, route }) => { const [iosConnected, setIosConnected] = useState(false); const [nasConnected, setNasConnected] = useState(false); + // Service health: 'inactive' | 'healthy' | 'error' | 'checking' + const [googleHealth, setGoogleHealth] = useState('inactive'); + const [iosHealth, setIosHealth] = useState('inactive'); + const [nasHealth, setNasHealth] = useState('inactive'); + // NAS config const [nasHost, setNasHost] = useState(''); const [nasPort, setNasPort] = useState('22'); @@ -52,6 +58,7 @@ const OnboardingScreen = ({ navigation, route }) => { const handleGoogleAuth = async () => { try { setLoading(true); + setGoogleHealth('checking'); const googleAuth = new GoogleAUTH(); const result = await googleAuth.authenticate(); @@ -62,9 +69,11 @@ const OnboardingScreen = ({ navigation, route }) => { }); setGoogleConnected(true); + setGoogleHealth('healthy'); Alert.alert('Success', 'Google Photos connected!'); } catch (error) { console.error('Google auth error:', error); + setGoogleHealth('error'); Alert.alert('Error', 'Failed to connect Google Photos: ' + error.message); } finally { setLoading(false); @@ -74,14 +83,17 @@ const OnboardingScreen = ({ navigation, route }) => { const handleIosAuth = async () => { try { setLoading(true); + setIosHealth('checking'); await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { status: 'placeholder', note: 'iCloud integration pending Apple API access', }); setIosConnected(true); + setIosHealth('healthy'); Alert.alert('Info', 'iCloud Photos registered. Full integration coming soon.'); } catch (error) { console.error('iCloud auth error:', error); + setIosHealth('error'); Alert.alert('Error', 'Failed to set up iCloud: ' + error.message); } finally { setLoading(false); @@ -95,6 +107,7 @@ const OnboardingScreen = ({ navigation, route }) => { } try { setLoading(true); + setNasHealth('checking'); const nasAuth = new NASAUTH(); await nasAuth.authenticate( nasHost.trim(), parseInt(nasPort) || 22, @@ -107,9 +120,11 @@ const OnboardingScreen = ({ navigation, route }) => { photoFolder: nasPhotoFolder.trim(), }); setNasConnected(true); + setNasHealth('healthy'); Alert.alert('Success', 'NAS connected!'); } catch (error) { console.error('NAS auth error:', error); + setNasHealth('error'); Alert.alert('Error', 'Failed to connect NAS: ' + error.message); } finally { setLoading(false); @@ -187,6 +202,7 @@ const OnboardingScreen = ({ navigation, route }) => { {googleEnabled && ( + 📷 Google Photos {googleConnected && ✓ Connected} @@ -205,6 +221,7 @@ const OnboardingScreen = ({ navigation, route }) => { {iosEnabled && ( + 🍎 iCloud Photos {iosConnected && ✓ Connected} @@ -223,6 +240,7 @@ const OnboardingScreen = ({ navigation, route }) => { {nasEnabled && ( + 💾 NAS Storage {nasConnected && ✓ Connected} diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js index 9d9789e..527e635 100644 --- a/src/screens/SettingsScreen.js +++ b/src/screens/SettingsScreen.js @@ -7,16 +7,21 @@ import { useFocusEffect } from '@react-navigation/native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; import NASSetupInstructions from '../components/NASSetupInstructions'; +import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; import NASAUTH from '../AUTH/NASAUTH'; +import GoogleAUTH from '../AUTH/GoogleAUTH'; const SettingsScreen = ({ navigation, route }) => { const userId = route?.params?.userId; - const { theme, themeName, setThemeName, themes } = useTheme(); + const { theme, themeName, setThemeName } = useTheme(); const [loading, setLoading] = useState(false); const [accounts, setAccounts] = useState([]); const [autoBackup, setAutoBackup] = useState([]); const [nasOffload, setNasOffload] = useState(false); + // Per-service health: { GOOGLE_PHOTOS: 'healthy'|'error'|'inactive', ... } + const [serviceHealth, setServiceHealth] = useState({}); + // NAS edit fields const [editingNas, setEditingNas] = useState(false); const [nasHost, setNasHost] = useState(''); @@ -45,20 +50,131 @@ const SettingsScreen = ({ navigation, route }) => { setNasUsername(nasAcc.account_data.username || ''); setNasPhotoFolder(nasAcc.account_data.photoFolder || '/home/user/photos'); } + + // Check health for each enabled account + checkAllServiceHealth(allAccounts); } catch (err) { console.error('Error loading settings:', err); } }; + // ─── Health checks ───────────────────────────────────────── + const checkAllServiceHealth = async (accs) => { + const health = {}; + for (const acc of accs) { + if (!acc.is_enabled) { + health[acc.service_type] = 'inactive'; + continue; + } + health[acc.service_type] = 'checking'; + setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); + health[acc.service_type] = await checkSingleHealth(acc); + } + setServiceHealth(health); + }; + + const checkSingleHealth = async (acc) => { + try { + if (acc.service_type === 'GOOGLE_PHOTOS') { + const gAuth = new GoogleAUTH(); + gAuth.accessToken = acc.account_data?.accessToken; + gAuth.refreshToken = acc.account_data?.refreshToken; + gAuth.expiresAt = Date.now() + 3600000; + const res = await gAuth.getPhotos(1); + return res?.mediaItems !== undefined ? 'healthy' : 'error'; + } + if (acc.service_type === 'NAS') { + const nasAuth = new NASAUTH(); + const d = acc.account_data || {}; + await nasAuth.authenticate(d.host, d.port, d.username, d.password, d.photoFolder); + await nasAuth.revokeAccess(); + return 'healthy'; + } + if (acc.service_type === 'ICLOUD_PHOTOS') { + // Placeholder – always "healthy" since it's a stub + return 'healthy'; + } + return 'inactive'; + } catch { + return 'error'; + } + }; + + const recheckHealth = async (acc) => { + setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); + const status = await checkSingleHealth(acc); + setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); + }; + + // ─── Service actions ─────────────────────────────────────── const toggleService = async (accountId, currentEnabled) => { try { await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); await loadData(); - } catch (err) { + } catch { Alert.alert('Error', 'Failed to update service'); } }; + const deleteAccount = (acc) => { + Alert.alert( + 'Delete Service', + `Remove ${getServiceName(acc.service_type)} from your account? This cannot be undone.`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + try { + await DatabaseService.removeLinkedAccount(acc.id); + setServiceHealth((prev) => { + const next = { ...prev }; + delete next[acc.service_type]; + return next; + }); + await loadData(); + Alert.alert('Deleted', `${getServiceName(acc.service_type)} has been removed.`); + } catch { + Alert.alert('Error', 'Failed to remove service'); + } + }, + }, + ] + ); + }; + + const reauthenticateService = async (acc) => { + setLoading(true); + try { + if (acc.service_type === 'GOOGLE_PHOTOS') { + const gAuth = new GoogleAUTH(); + const result = await gAuth.authenticate(); + await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { + accessToken: result.accessToken, + refreshToken: result.refreshToken || null, + email: result.email || 'google-user', + }); + Alert.alert('Success', 'Google Photos re-authenticated!'); + } else if (acc.service_type === 'ICLOUD_PHOTOS') { + await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { + status: 'placeholder', + note: 'iCloud integration pending Apple API access', + }); + Alert.alert('Info', 'iCloud Photos re-registered (placeholder).'); + } else if (acc.service_type === 'NAS') { + setEditingNas(true); + setLoading(false); + return; // user edits inline + } + await loadData(); + } catch (error) { + Alert.alert('Error', 'Re-authentication failed: ' + error.message); + } finally { + setLoading(false); + } + }; + const toggleAutoBackup = async (serviceType) => { const updated = autoBackup.includes(serviceType) ? autoBackup.filter((s) => s !== serviceType) @@ -130,7 +246,7 @@ const SettingsScreen = ({ navigation, route }) => { - {/* Theme Section */} + {/* ── Theme ──────────────────────────────────────── */} APPEARANCE {themeOptions.map((opt) => ( @@ -153,7 +269,7 @@ const SettingsScreen = ({ navigation, route }) => { ))} - {/* Services Section */} + {/* ── Services ──────────────────────────────────── */} SERVICES {accounts.length === 0 ? ( @@ -163,36 +279,60 @@ const SettingsScreen = ({ navigation, route }) => { ) : ( - accounts.map((acc) => ( - - {getServiceIcon(acc.service_type)} - - - {getServiceName(acc.service_type)} - - - {acc.is_enabled ? 'Enabled' : 'Disabled'} - + accounts.map((acc) => { + const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive'); + return ( + + {/* Main row */} + + recheckHealth(acc)} activeOpacity={0.6}> + + + {getServiceIcon(acc.service_type)} + + + {getServiceName(acc.service_type)} + + + {acc.is_enabled ? 'Enabled' : 'Disabled'} + + + toggleService(acc.id, acc.is_enabled)} + trackColor={{ false: theme.border, true: '#a8dab5' }} + thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} + /> + + {/* Action buttons row */} + + {acc.service_type === 'NAS' && acc.is_enabled && ( + setEditingNas(!editingNas)} + > + Edit + + )} + reauthenticateService(acc)} + disabled={loading} + > + + {loading ? '...' : 'Re-auth'} + + + deleteAccount(acc)} + > + Delete + + - {acc.service_type === 'NAS' && acc.is_enabled && ( - setEditingNas(!editingNas)} - > - Edit - - )} - toggleService(acc.id, acc.is_enabled)} - trackColor={{ false: theme.border, true: '#a8dab5' }} - thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} - /> - - )) + ); + }) )} @@ -248,7 +388,7 @@ const SettingsScreen = ({ navigation, route }) => { )} - {/* Auto Backup */} + {/* ── Auto Backup ───────────────────────────────── */} AUTOMATIC BACKUP @@ -309,7 +449,7 @@ const SettingsScreen = ({ navigation, route }) => { - {/* Sign Out */} + {/* ── Sign Out ──────────────────────────────────── */}