managarten/apps/picture/MIGRATION_PLAN.md
Till JS 22a73943e1 chore: complete ManaCore → Mana rename (docs, go modules, plists, images)
Final cleanup of references missed in previous rename commits:

- Dockerfiles: PUBLIC_MANA_CORE_AUTH_URL → PUBLIC_MANA_AUTH_URL
- Go modules: github.com/manacore/* → github.com/mana/* (7 go.mod files)
- launchd plists: com.manacore.* → com.mana.* (14 files renamed + content)
- Image assets: *_Manacore_AI_Credits* → *_Mana_AI_Credits* (11 files)
- .env.example files: ManaCore brand strings → Mana
- .prettierignore: stale apps/manacore/* paths → apps/mana/*
- Markdown docs (CLAUDE.md, /docs/*): mana-core-auth → mana-auth, etc.

Excluded from rename: .claude/, devlog/, manascore/ (historical content),
client testimonials, blueprints, npm package refs (@mana-core/*).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:26:10 +02:00

300 lines
8.6 KiB
Markdown

# Picture App: Supabase zu Backend API Migration
## Ziel
Komplette Entfernung der direkten Supabase-Nutzung in der Mobile App. Alle Datenbankzugriffe sollen über die Backend API erfolgen (wie in der Chat App).
---
## Aktuelle Situation
### Backend (bereits implementiert)
| Endpoint | Status |
|----------|--------|
| `/api/images/*` | Vorhanden - CRUD, Archive, Batch-Operationen |
| `/api/generate/*` | Vorhanden - Bildgenerierung mit Replicate |
| `/api/tags/*` | Vorhanden - Tag-Management |
| `/api/boards/*` | Vorhanden - Board-Management |
| `/api/board-items/*` | Vorhanden - Board-Items |
| `/api/models/*` | Vorhanden - AI Modelle |
| `/api/explore/*` | Vorhanden - Öffentliche Galerie |
| `/api/upload/*` | Vorhanden - Datei-Upload |
| `/api/profiles/*` | **FEHLT** - User Profile |
| `/api/likes/*` | **FEHLT** - Image Likes |
### Mobile App (direkte Supabase-Nutzung)
Dateien die `supabase.from()` oder Supabase-Client nutzen:
1. `app/(tabs)/profile.tsx` - Profile-Daten laden/aktualisieren
2. `app/image/[id].tsx` - Einzelbild-Details
3. `hooks/useImageFetching.ts` - Bilder laden
4. `hooks/useImageLikes.ts` - Like-Funktionalität
5. `hooks/useImagePrefetch.ts` - Prefetching
6. `hooks/useExploreFetching.ts` - Explore-Daten
7. `hooks/useExplorePrefetch.ts` - Explore-Prefetching
8. `hooks/useArchiveFetching.ts` - Archiv-Daten
9. `store/tagStore.ts` - Tag-Daten
10. `store/batchStore.ts` - Batch-Generierung (nutzt Edge Functions)
11. `components/RateLimitIndicator.tsx` - Rate Limit Check
---
## Migrationsplan
### Phase 1: Backend erweitern
#### 1.1 Profile Endpoints hinzufügen
```
GET /api/profiles/me - Eigenes Profil laden
PATCH /api/profiles/me - Profil aktualisieren
GET /api/profiles/stats - User Stats (Bilder, Favoriten)
```
**Schema erweitern** (falls nicht vorhanden):
```typescript
// profiles table
{
id: uuid (PK, same as auth user id)
username: text
email: text
avatarUrl: text (optional)
createdAt: timestamp
updatedAt: timestamp
}
```
#### 1.2 Like Endpoints hinzufügen
```
POST /api/images/:id/like - Bild liken
DELETE /api/images/:id/like - Like entfernen
GET /api/images/:id/likes - Like-Status & Anzahl
```
**Schema erweitern** (falls nicht vorhanden):
```typescript
// image_likes table
{
id: uuid (PK)
imageId: uuid (FK to images)
userId: uuid
createdAt: timestamp
}
```
#### 1.3 Rate Limit Endpoint
```
GET /api/rate-limit - Aktueller Rate Limit Status
```
---
### Phase 2: Mobile API Client erstellen
#### 2.1 Zentraler API Client
Datei: `services/api/client.ts`
```typescript
import * as SecureStore from 'expo-secure-store';
const APP_TOKEN_KEY = '@mana/app_token';
const BACKEND_URL = process.env.EXPO_PUBLIC_PICTURE_BACKEND_URL || 'http://localhost:3003';
async function getAuthToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync(APP_TOKEN_KEY);
} catch {
return null;
}
}
export async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ data: T | null; error: string | null }> {
const token = await getAuthToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(options.headers || {}),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(`${BACKEND_URL}/api${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }));
return { data: null, error: error.message || `HTTP ${response.status}` };
}
const data = await response.json();
return { data, error: null };
} catch (error) {
return { data: null, error: error instanceof Error ? error.message : 'Network error' };
}
}
```
#### 2.2 Domain-spezifische API Module
**`services/api/profiles.ts`**
```typescript
export const profileApi = {
getMyProfile: () => apiRequest<Profile>('/profiles/me'),
updateProfile: (data: UpdateProfileDto) => apiRequest<Profile>('/profiles/me', {
method: 'PATCH',
body: JSON.stringify(data),
}),
getStats: () => apiRequest<UserStats>('/profiles/stats'),
};
```
**`services/api/images.ts`** (erweitern)
```typescript
export const imageApi = {
// Bestehende Funktionen...
getImages: (params) => apiRequest<Image[]>(`/images?${new URLSearchParams(params)}`),
getImage: (id) => apiRequest<Image>(`/images/${id}`),
likeImage: (id) => apiRequest(`/images/${id}/like`, { method: 'POST' }),
unlikeImage: (id) => apiRequest(`/images/${id}/like`, { method: 'DELETE' }),
// etc.
};
```
**`services/api/explore.ts`**
```typescript
export const exploreApi = {
getPublicImages: (params) => apiRequest<Image[]>(`/explore?${new URLSearchParams(params)}`),
search: (term, params) => apiRequest<Image[]>(`/explore/search?searchTerm=${term}&${new URLSearchParams(params)}`),
};
```
**`services/api/tags.ts`**
```typescript
export const tagApi = {
getTags: () => apiRequest<Tag[]>('/tags'),
createTag: (data) => apiRequest<Tag>('/tags', { method: 'POST', body: JSON.stringify(data) }),
updateTag: (id, data) => apiRequest<Tag>(`/tags/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
deleteTag: (id) => apiRequest(`/tags/${id}`, { method: 'DELETE' }),
getImageTags: (imageId) => apiRequest<Tag[]>(`/tags/image/${imageId}`),
addTagToImage: (imageId, tagId) => apiRequest(`/tags/image/${imageId}/${tagId}`, { method: 'POST' }),
removeTagFromImage: (imageId, tagId) => apiRequest(`/tags/image/${imageId}/${tagId}`, { method: 'DELETE' }),
};
```
---
### Phase 3: Hooks migrieren
#### 3.1 `useImageFetching.ts`
- Ersetze `supabase.from('images')` durch `imageApi.getImages()`
- Pagination über API Query-Parameter
#### 3.2 `useImageLikes.ts`
- Ersetze `supabase.from('image_likes')` durch `imageApi.likeImage()` / `unlikeImage()`
#### 3.3 `useExploreFetching.ts`
- Ersetze `supabase.from('images').eq('is_public', true)` durch `exploreApi.getPublicImages()`
#### 3.4 `useArchiveFetching.ts`
- Ersetze `supabase.from('images').not('archived_at', 'is', null)` durch `imageApi.getImages({ archived: true })`
---
### Phase 4: Stores migrieren
#### 4.1 `tagStore.ts`
- Ersetze alle `supabase.from('tags')` und `supabase.from('image_tags')` durch `tagApi.*`
#### 4.2 `batchStore.ts`
- Ersetze `supabase.functions.invoke()` durch Backend API Calls
- Ersetze `supabase.channel()` Realtime durch Polling oder WebSocket zum Backend
- **ODER**: Batch-Generierung komplett über Backend API abwickeln
---
### Phase 5: Screens migrieren
#### 5.1 `profile.tsx`
```typescript
// Vorher:
const { data } = await supabase.from('profiles').select('*').eq('id', user.id).single();
// Nachher:
const { data } = await profileApi.getMyProfile();
```
#### 5.2 `image/[id].tsx`
```typescript
// Vorher:
const { data } = await supabase.from('images').select('*').eq('id', id).single();
// Nachher:
const { data } = await imageApi.getImage(id);
```
---
### Phase 6: Aufräumen
#### 6.1 Dateien entfernen
- `utils/supabase.ts` - Supabase Client
- Alle Supabase-Typen die nicht mehr gebraucht werden
#### 6.2 Dependencies entfernen
```json
// package.json - Diese entfernen:
"@supabase/supabase-js": "^2.38.4",
```
#### 6.3 Environment Variables aufräumen
```
# Nicht mehr benötigt:
EXPO_PUBLIC_SUPABASE_URL
EXPO_PUBLIC_SUPABASE_ANON_KEY
# Weiterhin benötigt:
EXPO_PUBLIC_PICTURE_BACKEND_URL
EXPO_PUBLIC_MANA_AUTH_URL
```
---
## Implementierungsreihenfolge
1. **Backend erweitern** (Profile, Likes, Rate Limit Endpoints)
2. **API Client erstellen** (`services/api/client.ts`)
3. **Domain APIs erstellen** (profiles, images erweitern, explore, tags)
4. **Hooks einzeln migrieren** (mit Tests nach jeder Migration)
5. **Stores migrieren** (tagStore, batchStore)
6. **Screens migrieren** (profile.tsx, image/[id].tsx)
7. **Supabase entfernen** (Dependencies, Environment Variables)
8. **Testen** (Alle Flows durchgehen)
---
## Geschätzter Aufwand
| Phase | Aufwand |
|-------|---------|
| Phase 1: Backend erweitern | ~2-3 Stunden |
| Phase 2: API Client | ~1 Stunde |
| Phase 3: Hooks migrieren | ~2-3 Stunden |
| Phase 4: Stores migrieren | ~1-2 Stunden |
| Phase 5: Screens migrieren | ~1 Stunde |
| Phase 6: Aufräumen & Testen | ~1 Stunde |
| **Gesamt** | **~8-11 Stunden** |
---
## Risiken & Hinweise
1. **Realtime-Subscriptions**: Supabase Realtime wird durch Polling ersetzt (bereits bei Bildgenerierung umgesetzt)
2. **Batch-Generierung**: Der `batchStore` nutzt Edge Functions - diese müssen ins Backend migriert werden
3. **Storage**: Bilder werden weiterhin irgendwo gespeichert - prüfen ob S3/R2 oder weiterhin Supabase Storage
4. **RLS Policies**: Backend übernimmt die Autorisierung - alle Queries müssen `userId` filtern