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:
Till-JS 2025-11-29 07:03:59 +01:00
parent b97149ac12
commit 61d181fbc2
3148 changed files with 437 additions and 46640 deletions

View 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;
}

View file

@ -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);
}
}

View 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 {}

View 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(),
};
}
}