mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 19:46:42 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
345
apps-archived/nutriphi/apps/mobile/services/sync/SyncService.ts
Normal file
345
apps-archived/nutriphi/apps/mobile/services/sync/SyncService.ts
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
import { tokenManager } from '../auth/tokenManager';
|
||||
import { SQLiteService } from '../database/SQLiteService';
|
||||
import type { Meal } from '../../types/Database';
|
||||
|
||||
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3002';
|
||||
|
||||
export interface SyncResult {
|
||||
success: boolean;
|
||||
created: number;
|
||||
updated: number;
|
||||
deleted: number;
|
||||
conflicts: ConflictInfo[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ConflictInfo {
|
||||
cloudId: string;
|
||||
localVersion: number;
|
||||
serverVersion: number;
|
||||
serverData: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface LocalMealForSync {
|
||||
localId: number;
|
||||
cloudId?: string;
|
||||
foodName: string;
|
||||
imageUrl?: string;
|
||||
calories?: number;
|
||||
protein?: number;
|
||||
carbohydrates?: number;
|
||||
fat?: number;
|
||||
fiber?: number;
|
||||
sugar?: number;
|
||||
sodium?: number;
|
||||
servingSize?: string;
|
||||
mealType?: string;
|
||||
analysisStatus?: string;
|
||||
healthScore?: number;
|
||||
healthCategory?: string;
|
||||
notes?: string;
|
||||
userRating?: number;
|
||||
foodItems?: any[];
|
||||
version: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Service for synchronizing local SQLite data with the backend
|
||||
*/
|
||||
export class SyncService {
|
||||
private static instance: SyncService;
|
||||
private isSyncing = false;
|
||||
private lastSyncAt: string | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): SyncService {
|
||||
if (!SyncService.instance) {
|
||||
SyncService.instance = new SyncService();
|
||||
}
|
||||
return SyncService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sync is currently in progress
|
||||
*/
|
||||
public isSyncInProgress(): boolean {
|
||||
return this.isSyncing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last sync timestamp
|
||||
*/
|
||||
public getLastSyncAt(): string | null {
|
||||
return this.lastSyncAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a full sync (push + pull)
|
||||
*/
|
||||
public async fullSync(): Promise<SyncResult> {
|
||||
if (this.isSyncing) {
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: 'Sync already in progress',
|
||||
};
|
||||
}
|
||||
|
||||
this.isSyncing = true;
|
||||
|
||||
try {
|
||||
// First push local changes
|
||||
const pushResult = await this.pushChanges();
|
||||
if (!pushResult.success) {
|
||||
return pushResult;
|
||||
}
|
||||
|
||||
// Then pull server changes
|
||||
const pullResult = await this.pullChanges();
|
||||
|
||||
return {
|
||||
success: pullResult.success,
|
||||
created: pushResult.created + pullResult.created,
|
||||
updated: pushResult.updated + pullResult.updated,
|
||||
deleted: pullResult.deleted,
|
||||
conflicts: pushResult.conflicts,
|
||||
error: pullResult.error,
|
||||
};
|
||||
} finally {
|
||||
this.isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push local changes to server
|
||||
*/
|
||||
public async pushChanges(): Promise<SyncResult> {
|
||||
try {
|
||||
const authHeader = await tokenManager.getAuthHeader();
|
||||
if (!authHeader.Authorization) {
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
const db = SQLiteService.getInstance();
|
||||
|
||||
// Get unsynced meals
|
||||
const unsyncedMeals = await db.getUnsyncedMeals();
|
||||
|
||||
if (unsyncedMeals.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Map to sync format
|
||||
const mealsForSync: LocalMealForSync[] = unsyncedMeals.map((meal) =>
|
||||
this.mapMealToSyncFormat(meal)
|
||||
);
|
||||
|
||||
// Get deleted meals (meals marked for deletion)
|
||||
const deletedIds: string[] = []; // TODO: Implement delete tracking
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/sync/push`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
meals: mealsForSync,
|
||||
deletedIds,
|
||||
lastSyncAt: this.lastSyncAt,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: error.message || 'Push failed',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update local records with cloud IDs
|
||||
for (const created of result.created) {
|
||||
await db.updateCloudId(created.localId, created.cloudId);
|
||||
await db.markSynced(created.localId);
|
||||
}
|
||||
|
||||
// Mark updated records as synced
|
||||
for (const cloudId of result.updated) {
|
||||
const meal = unsyncedMeals.find((m) => m.cloud_id === cloudId);
|
||||
if (meal && meal.id) {
|
||||
await db.markSynced(meal.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastSyncAt = result.serverTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
created: result.created.length,
|
||||
updated: result.updated.length,
|
||||
deleted: 0,
|
||||
conflicts: result.conflicts || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Push sync error:', error);
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: error instanceof Error ? error.message : 'Push failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull changes from server
|
||||
*/
|
||||
public async pullChanges(): Promise<SyncResult> {
|
||||
try {
|
||||
const authHeader = await tokenManager.getAuthHeader();
|
||||
if (!authHeader.Authorization) {
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
const url = new URL(`${BACKEND_URL}/api/sync/pull`);
|
||||
if (this.lastSyncAt) {
|
||||
url.searchParams.set('since', this.lastSyncAt);
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeader,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: error.message || 'Pull failed',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const db = SQLiteService.getInstance();
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let deleted = 0;
|
||||
|
||||
// Process server meals
|
||||
for (const serverMeal of result.meals) {
|
||||
const existingMeal = await db.getMealByCloudId(serverMeal.cloudId);
|
||||
|
||||
if (existingMeal) {
|
||||
// Update existing local meal
|
||||
await db.updateMealFromServer(existingMeal.id!, serverMeal);
|
||||
updated++;
|
||||
} else {
|
||||
// Create new local meal
|
||||
await db.createMealFromServer(serverMeal);
|
||||
created++;
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
for (const cloudId of result.deletedIds) {
|
||||
await db.deleteByCloudId(cloudId);
|
||||
deleted++;
|
||||
}
|
||||
|
||||
this.lastSyncAt = result.serverTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
created,
|
||||
updated,
|
||||
deleted,
|
||||
conflicts: [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Pull sync error:', error);
|
||||
return {
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
conflicts: [],
|
||||
error: error instanceof Error ? error.message : 'Pull failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map local meal to sync format
|
||||
*/
|
||||
private mapMealToSyncFormat(meal: Meal): LocalMealForSync {
|
||||
return {
|
||||
localId: meal.id!,
|
||||
cloudId: meal.cloud_id || undefined,
|
||||
foodName: meal.analysis_result
|
||||
? JSON.parse(meal.analysis_result).foodName || 'Unbekanntes Gericht'
|
||||
: 'Unbekanntes Gericht',
|
||||
imageUrl: meal.photo_url || undefined,
|
||||
calories: meal.total_calories || undefined,
|
||||
protein: meal.total_protein || undefined,
|
||||
carbohydrates: meal.total_carbs || undefined,
|
||||
fat: meal.total_fat || undefined,
|
||||
fiber: meal.total_fiber || undefined,
|
||||
sugar: meal.total_sugar || undefined,
|
||||
servingSize: undefined,
|
||||
mealType: meal.meal_type || undefined,
|
||||
analysisStatus: meal.analysis_status || 'completed',
|
||||
healthScore: meal.health_score || undefined,
|
||||
healthCategory: meal.health_category || undefined,
|
||||
notes: meal.user_notes || undefined,
|
||||
userRating: meal.user_rating || undefined,
|
||||
foodItems: meal.analysis_result ? JSON.parse(meal.analysis_result).foodItems : [],
|
||||
version: meal.version || 1,
|
||||
createdAt: meal.created_at || new Date().toISOString(),
|
||||
updatedAt: meal.updated_at || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue