mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 22:26:41 +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
153
apps-archived/nutriphi/apps/backend/src/sync/dto/sync.dto.ts
Normal file
153
apps-archived/nutriphi/apps/backend/src/sync/dto/sync.dto.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsNumber,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* Local meal data from mobile app
|
||||
*/
|
||||
export class LocalMealDto {
|
||||
@IsNumber()
|
||||
localId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cloudId?: string;
|
||||
|
||||
@IsString()
|
||||
foodName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
imageUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
calories?: number;
|
||||
|
||||
@IsOptional()
|
||||
protein?: number;
|
||||
|
||||
@IsOptional()
|
||||
carbohydrates?: number;
|
||||
|
||||
@IsOptional()
|
||||
fat?: number;
|
||||
|
||||
@IsOptional()
|
||||
fiber?: number;
|
||||
|
||||
@IsOptional()
|
||||
sugar?: number;
|
||||
|
||||
@IsOptional()
|
||||
sodium?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
servingSize?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mealType?: 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
analysisStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
healthScore?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
healthCategory?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsOptional()
|
||||
userRating?: number;
|
||||
|
||||
@IsOptional()
|
||||
foodItems?: any[];
|
||||
|
||||
@IsNumber()
|
||||
version: number;
|
||||
|
||||
@IsString()
|
||||
createdAt: string;
|
||||
|
||||
@IsString()
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push request - local changes to server
|
||||
*/
|
||||
export class SyncPushDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LocalMealDto)
|
||||
meals: LocalMealDto[];
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
deletedIds: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastSyncAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push response
|
||||
*/
|
||||
export interface SyncPushResponse {
|
||||
created: { localId: number; cloudId: string }[];
|
||||
updated: string[];
|
||||
conflicts: ConflictInfo[];
|
||||
serverTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict information
|
||||
*/
|
||||
export interface ConflictInfo {
|
||||
cloudId: string;
|
||||
localVersion: number;
|
||||
serverVersion: number;
|
||||
serverData: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull query parameters
|
||||
*/
|
||||
export class SyncPullQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
since?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull response
|
||||
*/
|
||||
export interface SyncPullResponse {
|
||||
meals: any[];
|
||||
deletedIds: string[];
|
||||
serverTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync status response
|
||||
*/
|
||||
export interface SyncStatusResponse {
|
||||
lastSyncAt: string | null;
|
||||
pendingChanges: number;
|
||||
serverTime: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
|
||||
import { SyncService } from './sync.service';
|
||||
import {
|
||||
SyncPushDto,
|
||||
SyncPushResponse,
|
||||
SyncPullQueryDto,
|
||||
SyncPullResponse,
|
||||
SyncStatusResponse,
|
||||
} from './dto/sync.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser, CurrentUserData } from '../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('sync')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SyncController {
|
||||
constructor(private readonly syncService: SyncService) {}
|
||||
|
||||
/**
|
||||
* Push local changes to server
|
||||
* POST /api/sync/push
|
||||
*/
|
||||
@Post('push')
|
||||
async pushChanges(
|
||||
@Body() dto: SyncPushDto,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
): Promise<SyncPushResponse> {
|
||||
return this.syncService.pushChanges(user.userId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull changes from server
|
||||
* GET /api/sync/pull?since=2024-01-01T00:00:00Z
|
||||
*/
|
||||
@Get('pull')
|
||||
async pullChanges(
|
||||
@Query() query: SyncPullQueryDto,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
): Promise<SyncPullResponse> {
|
||||
return this.syncService.pullChanges(user.userId, query.since);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status
|
||||
* GET /api/sync/status
|
||||
*/
|
||||
@Get('status')
|
||||
async getStatus(@CurrentUser() user: CurrentUserData): Promise<SyncStatusResponse> {
|
||||
return this.syncService.getStatus(user.userId);
|
||||
}
|
||||
}
|
||||
10
apps-archived/nutriphi/apps/backend/src/sync/sync.module.ts
Normal file
10
apps-archived/nutriphi/apps/backend/src/sync/sync.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SyncController } from './sync.controller';
|
||||
import { SyncService } from './sync.service';
|
||||
|
||||
@Module({
|
||||
controllers: [SyncController],
|
||||
providers: [SyncService],
|
||||
exports: [SyncService],
|
||||
})
|
||||
export class SyncModule {}
|
||||
246
apps-archived/nutriphi/apps/backend/src/sync/sync.service.ts
Normal file
246
apps-archived/nutriphi/apps/backend/src/sync/sync.service.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import {
|
||||
type Database,
|
||||
meals,
|
||||
eq,
|
||||
and,
|
||||
gt,
|
||||
type Meal as DbMeal,
|
||||
} from '@manacore/nutriphi-database';
|
||||
import { DATABASE_TOKEN } from '../database/database.module';
|
||||
import {
|
||||
LocalMealDto,
|
||||
SyncPushDto,
|
||||
SyncPushResponse,
|
||||
SyncPullResponse,
|
||||
SyncStatusResponse,
|
||||
ConflictInfo,
|
||||
} from './dto/sync.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SyncService {
|
||||
private readonly logger = new Logger(SyncService.name);
|
||||
|
||||
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
|
||||
|
||||
/**
|
||||
* Push local changes to server
|
||||
*/
|
||||
async pushChanges(userId: string, dto: SyncPushDto): Promise<SyncPushResponse> {
|
||||
this.logger.log(`Processing sync push for user: ${userId}, ${dto.meals.length} meals`);
|
||||
|
||||
const created: { localId: number; cloudId: string }[] = [];
|
||||
const updated: string[] = [];
|
||||
const conflicts: ConflictInfo[] = [];
|
||||
const serverTime = new Date().toISOString();
|
||||
|
||||
// Process each meal
|
||||
for (const localMeal of dto.meals) {
|
||||
try {
|
||||
if (localMeal.cloudId) {
|
||||
// Update existing meal
|
||||
const result = await this.updateExistingMeal(userId, localMeal);
|
||||
if (result.conflict) {
|
||||
conflicts.push(result.conflict);
|
||||
} else if (result.updated) {
|
||||
updated.push(localMeal.cloudId);
|
||||
}
|
||||
} else {
|
||||
// Create new meal
|
||||
const cloudId = await this.createNewMeal(userId, localMeal);
|
||||
created.push({ localId: localMeal.localId, cloudId });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing meal ${localMeal.localId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
for (const cloudId of dto.deletedIds) {
|
||||
try {
|
||||
await this.db.delete(meals).where(and(eq(meals.id, cloudId), eq(meals.userId, userId)));
|
||||
this.logger.log(`Deleted meal: ${cloudId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting meal ${cloudId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { created, updated, conflicts, serverTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull changes from server since given timestamp
|
||||
*/
|
||||
async pullChanges(userId: string, since?: string): Promise<SyncPullResponse> {
|
||||
this.logger.log(`Processing sync pull for user: ${userId}, since: ${since}`);
|
||||
|
||||
const serverTime = new Date().toISOString();
|
||||
|
||||
let query;
|
||||
if (since) {
|
||||
const sinceDate = new Date(since);
|
||||
query = this.db
|
||||
.select()
|
||||
.from(meals)
|
||||
.where(and(eq(meals.userId, userId), gt(meals.updatedAt, sinceDate)));
|
||||
} else {
|
||||
// Full sync - get all meals
|
||||
query = this.db.select().from(meals).where(eq(meals.userId, userId));
|
||||
}
|
||||
|
||||
const results = await query;
|
||||
|
||||
const mappedMeals = results.map((meal) => this.mapDbMealToSync(meal));
|
||||
|
||||
return {
|
||||
meals: mappedMeals,
|
||||
deletedIds: [], // TODO: Implement soft deletes to track deleted meals
|
||||
serverTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status
|
||||
*/
|
||||
async getStatus(userId: string): Promise<SyncStatusResponse> {
|
||||
const serverTime = new Date().toISOString();
|
||||
|
||||
// Count user's meals
|
||||
const result = await this.db.select().from(meals).where(eq(meals.userId, userId));
|
||||
|
||||
return {
|
||||
lastSyncAt: null, // Could be stored in a user preferences table
|
||||
pendingChanges: 0,
|
||||
serverTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new meal from local data
|
||||
*/
|
||||
private async createNewMeal(userId: string, localMeal: LocalMealDto): Promise<string> {
|
||||
const [result] = await this.db
|
||||
.insert(meals)
|
||||
.values({
|
||||
userId,
|
||||
foodName: localMeal.foodName,
|
||||
imageUrl: localMeal.imageUrl,
|
||||
calories: localMeal.calories ?? 0,
|
||||
protein: localMeal.protein ?? 0,
|
||||
carbohydrates: localMeal.carbohydrates ?? 0,
|
||||
fat: localMeal.fat ?? 0,
|
||||
fiber: localMeal.fiber ?? 0,
|
||||
sugar: localMeal.sugar ?? 0,
|
||||
sodium: localMeal.sodium ?? 0,
|
||||
servingSize: localMeal.servingSize,
|
||||
mealType: localMeal.mealType,
|
||||
analysisStatus: localMeal.analysisStatus ?? 'completed',
|
||||
healthScore: localMeal.healthScore,
|
||||
healthCategory: localMeal.healthCategory,
|
||||
notes: localMeal.notes,
|
||||
userRating: localMeal.userRating,
|
||||
foodItems: localMeal.foodItems ?? [],
|
||||
createdAt: new Date(localMeal.createdAt),
|
||||
updatedAt: new Date(localMeal.updatedAt),
|
||||
})
|
||||
.returning();
|
||||
|
||||
this.logger.log(`Created meal: ${result.id} for local: ${localMeal.localId}`);
|
||||
return result.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing meal, checking for conflicts
|
||||
*/
|
||||
private async updateExistingMeal(
|
||||
userId: string,
|
||||
localMeal: LocalMealDto
|
||||
): Promise<{ updated: boolean; conflict?: ConflictInfo }> {
|
||||
// Get current server version
|
||||
const [serverMeal] = await this.db
|
||||
.select()
|
||||
.from(meals)
|
||||
.where(and(eq(meals.id, localMeal.cloudId!), eq(meals.userId, userId)));
|
||||
|
||||
if (!serverMeal) {
|
||||
this.logger.warn(`Meal not found: ${localMeal.cloudId}`);
|
||||
return { updated: false };
|
||||
}
|
||||
|
||||
// Simple last-write-wins strategy
|
||||
// In production, you might want more sophisticated conflict resolution
|
||||
const localUpdateTime = new Date(localMeal.updatedAt);
|
||||
const serverUpdateTime = serverMeal.updatedAt;
|
||||
|
||||
// If local is newer, update server
|
||||
if (localUpdateTime >= serverUpdateTime) {
|
||||
await this.db
|
||||
.update(meals)
|
||||
.set({
|
||||
foodName: localMeal.foodName,
|
||||
imageUrl: localMeal.imageUrl,
|
||||
calories: localMeal.calories ?? 0,
|
||||
protein: localMeal.protein ?? 0,
|
||||
carbohydrates: localMeal.carbohydrates ?? 0,
|
||||
fat: localMeal.fat ?? 0,
|
||||
fiber: localMeal.fiber ?? 0,
|
||||
sugar: localMeal.sugar ?? 0,
|
||||
sodium: localMeal.sodium ?? 0,
|
||||
servingSize: localMeal.servingSize,
|
||||
mealType: localMeal.mealType,
|
||||
analysisStatus: localMeal.analysisStatus,
|
||||
healthScore: localMeal.healthScore,
|
||||
healthCategory: localMeal.healthCategory,
|
||||
notes: localMeal.notes,
|
||||
userRating: localMeal.userRating,
|
||||
foodItems: localMeal.foodItems ?? [],
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(meals.id, localMeal.cloudId!));
|
||||
|
||||
this.logger.log(`Updated meal: ${localMeal.cloudId}`);
|
||||
return { updated: true };
|
||||
}
|
||||
|
||||
// Server is newer - report conflict
|
||||
return {
|
||||
updated: false,
|
||||
conflict: {
|
||||
cloudId: localMeal.cloudId!,
|
||||
localVersion: localMeal.version,
|
||||
serverVersion: 1, // Would need version tracking in DB
|
||||
serverData: this.mapDbMealToSync(serverMeal),
|
||||
message: 'Server has newer data',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map database meal to sync format
|
||||
*/
|
||||
private mapDbMealToSync(meal: DbMeal): any {
|
||||
return {
|
||||
cloudId: meal.id,
|
||||
userId: meal.userId,
|
||||
foodName: meal.foodName,
|
||||
imageUrl: meal.imageUrl,
|
||||
calories: meal.calories,
|
||||
protein: meal.protein,
|
||||
carbohydrates: meal.carbohydrates,
|
||||
fat: meal.fat,
|
||||
fiber: meal.fiber,
|
||||
sugar: meal.sugar,
|
||||
sodium: meal.sodium,
|
||||
servingSize: meal.servingSize,
|
||||
mealType: meal.mealType,
|
||||
analysisStatus: meal.analysisStatus,
|
||||
healthScore: meal.healthScore,
|
||||
healthCategory: meal.healthCategory,
|
||||
notes: meal.notes,
|
||||
userRating: meal.userRating,
|
||||
foodItems: meal.foodItems,
|
||||
createdAt: meal.createdAt.toISOString(),
|
||||
updatedAt: meal.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue