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 ──────────────────────────────────── */}