feat: bidirectional sync — upload local activities to remote instance
- Server: POST /api/upload/bas accepts pre-extracted BAS JSON (activity + optional timeseries/geojson), writes files and triggers merge_all - sync.ts: uploadLocalActivities reads unsynced local activities by original_path, POSTs to /api/upload/bas, marks synced_at on success - Settings: Upload toggle (Off / Upload local activities) in Sync section with subLabel dividers for Download / Upload groups - Feed: sync message includes uploaded count when activities are pushed
This commit is contained in:
@@ -20,12 +20,13 @@ export default function FeedScreen() {
|
||||
setSyncMsg(result.error);
|
||||
} else if (result.total === 0) {
|
||||
setSyncMsg('No activities on instance');
|
||||
} else if (result.synced === 0 && !result.fetched) {
|
||||
} else if (result.synced === 0 && !result.fetched && !result.uploaded) {
|
||||
setSyncMsg(`Up to date (${result.total} activities)`);
|
||||
} else {
|
||||
const parts = [];
|
||||
if (result.synced > 0) parts.push(`${result.synced} new`);
|
||||
if (result.fetched) parts.push(`${result.fetched} full dataset${result.fetched === 1 ? '' : 's'}`);
|
||||
if (result.uploaded) parts.push(`${result.uploaded} uploaded`);
|
||||
setSyncMsg(`Synced: ${parts.join(', ')} (${result.total} total)`);
|
||||
}
|
||||
setTimeout(() => setSyncMsg(null), 3500);
|
||||
|
||||
@@ -13,7 +13,8 @@ export default function SettingsScreen() {
|
||||
const storedHandle = useSetting('handle') ?? '';
|
||||
const storedPath = useSetting('auto_import_path') ?? '';
|
||||
const storedToken = useSetting('api_token');
|
||||
const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full';
|
||||
const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full';
|
||||
const storedSyncUpload = useSetting('sync_upload') === 'true';
|
||||
|
||||
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
|
||||
const [handle, setHandle] = useState(storedHandle);
|
||||
@@ -176,6 +177,7 @@ export default function SettingsScreen() {
|
||||
)}
|
||||
|
||||
<Section title="Sync">
|
||||
<Text style={styles.subLabel}>Download</Text>
|
||||
<View style={styles.modeRow}>
|
||||
<ModeButton
|
||||
label="Summaries only"
|
||||
@@ -193,6 +195,24 @@ export default function SettingsScreen() {
|
||||
? 'Downloads map route and elevation chart for every activity during sync. Uses more storage and takes longer.'
|
||||
: 'Syncs activity summaries only. Map and chart are fetched on demand when you open an activity.'}
|
||||
</Text>
|
||||
<Text style={[styles.subLabel, { borderTopWidth: 1, borderTopColor: '#27272a', paddingTop: 12 }]}>Upload</Text>
|
||||
<View style={styles.modeRow}>
|
||||
<ModeButton
|
||||
label="Off"
|
||||
active={!storedSyncUpload}
|
||||
onPress={() => setSetting(db, 'sync_upload', 'false')}
|
||||
/>
|
||||
<ModeButton
|
||||
label="Upload local activities"
|
||||
active={storedSyncUpload}
|
||||
onPress={() => setSetting(db, 'sync_upload', 'true')}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.hint}>
|
||||
{storedSyncUpload
|
||||
? 'Local activities are uploaded to the instance during sync.'
|
||||
: 'Local activities stay on device only.'}
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section title="Data">
|
||||
@@ -314,6 +334,7 @@ const styles = StyleSheet.create({
|
||||
disconnectText: { color: '#71717a', fontSize: 14 },
|
||||
msgOk: { color: '#86efac', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 },
|
||||
msgErr: { color: '#fca5a5', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 },
|
||||
subLabel: { color: '#52525b', fontSize: 11, fontWeight: '600', letterSpacing: 0.6, paddingHorizontal: 12, paddingTop: 12, paddingBottom: 4 },
|
||||
modeRow: { flexDirection: 'row', gap: 8, padding: 12 },
|
||||
modeButton: { flex: 1, paddingVertical: 9, borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center' },
|
||||
modeButtonActive: { backgroundColor: '#1e3a5f', borderColor: '#2563eb' },
|
||||
|
||||
Reference in New Issue
Block a user