docs: add Sub-Dokumentation section to AGENTS.md and CLAUDE.md for improved documentation structure
- Introduced a new section detailing the Single Source of Truth for various components, linking to their respective CLAUDE.md files. - Enhanced clarity on documentation organization across different areas of the project.
This commit is contained in:
15
AGENTS.md
15
AGENTS.md
@@ -3,3 +3,18 @@
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
|
||||
## Sub-Dokumentation
|
||||
|
||||
Jeder Ordner hat eine eigene CLAUDE.md als Single Source of Truth. Vor Änderungen immer das zugehörige Dok lesen.
|
||||
|
||||
| Bereich | Dok |
|
||||
|---------|-----|
|
||||
| Convex Backend | [convex/CLAUDE.md](convex/CLAUDE.md) |
|
||||
| Canvas-Engine | [components/canvas/CLAUDE.md](components/canvas/CLAUDE.md) |
|
||||
| Design System (ShadCN) | [components/ui/CLAUDE.md](components/ui/CLAUDE.md) |
|
||||
| Billing & Credits UI | [components/billing/CLAUDE.md](components/billing/CLAUDE.md) |
|
||||
| Dashboard | [components/dashboard/CLAUDE.md](components/dashboard/CLAUDE.md) |
|
||||
| Next.js Routing | [app/CLAUDE.md](app/CLAUDE.md) |
|
||||
| Utilities & Shared Logic | [lib/CLAUDE.md](lib/CLAUDE.md) |
|
||||
| Custom Hooks | [hooks/CLAUDE.md](hooks/CLAUDE.md) |
|
||||
|
||||
17
CLAUDE.md
17
CLAUDE.md
@@ -1,5 +1,22 @@
|
||||
@AGENTS.md
|
||||
|
||||
## Sub-Dokumentation
|
||||
|
||||
Jeder Ordner hat eine eigene CLAUDE.md als Single Source of Truth:
|
||||
|
||||
| Bereich | Dok |
|
||||
|---------|-----|
|
||||
| Convex Backend | [convex/CLAUDE.md](convex/CLAUDE.md) |
|
||||
| Canvas-Engine | [components/canvas/CLAUDE.md](components/canvas/CLAUDE.md) |
|
||||
| Design System (ShadCN) | [components/ui/CLAUDE.md](components/ui/CLAUDE.md) |
|
||||
| Billing & Credits UI | [components/billing/CLAUDE.md](components/billing/CLAUDE.md) |
|
||||
| Dashboard | [components/dashboard/CLAUDE.md](components/dashboard/CLAUDE.md) |
|
||||
| Next.js Routing | [app/CLAUDE.md](app/CLAUDE.md) |
|
||||
| Utilities & Shared Logic | [lib/CLAUDE.md](lib/CLAUDE.md) |
|
||||
| Custom Hooks | [hooks/CLAUDE.md](hooks/CLAUDE.md) |
|
||||
|
||||
---
|
||||
|
||||
## Design Context
|
||||
|
||||
### Users
|
||||
|
||||
91
app/CLAUDE.md
Normal file
91
app/CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# app/ — Next.js Routing & Layouts
|
||||
|
||||
Next.js 16 App Router. Vor Änderungen: Relevante Guides in `node_modules/next/dist/docs/` lesen — Breaking Changes gegenüber früheren Versionen möglich.
|
||||
|
||||
---
|
||||
|
||||
## Route-Struktur
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx ← Root Layout (Fonts, Providers, Sentry, Analytics)
|
||||
├── page.tsx ← Landing / Redirect
|
||||
├── globals.css ← Tailwind v4 + Design-Tokens
|
||||
├── (app)/ ← Authentifizierte App-Routen
|
||||
│ ├── canvas/[canvasId]/ ← Canvas-Editor
|
||||
│ │ └── page.tsx
|
||||
│ └── settings/
|
||||
│ └── billing/ ← Billing-Einstellungen
|
||||
├── auth/ ← Auth-Routen (Better Auth)
|
||||
│ ├── [path]/ ← Catch-all für Better Auth Flows
|
||||
│ ├── sign-in/
|
||||
│ └── sign-up/
|
||||
├── api/
|
||||
│ ├── auth/[...all]/ ← Better Auth API Handler
|
||||
│ └── pexels-video/ ← Pexels Video Proxy
|
||||
└── dashboard/ ← Dashboard nach Login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Layout (`layout.tsx`)
|
||||
|
||||
Server Component. Initialisiert:
|
||||
|
||||
1. **Font:** `Manrope` via `next/font/google` → CSS-Variable `--font-sans`
|
||||
2. **Auth:** `getToken()` + `getAuthUser()` für SSR-seitigen Convex-Token
|
||||
3. **Sentry:** `Sentry.setUser()` direkt im Layout (serverseitig)
|
||||
4. **Analytics:** Rybbit-Script via `<script>` Tag (`rybbit.matthias.lol`)
|
||||
5. **Providers:** `<Providers initialToken={...}>` — Convex + Theme + Auth
|
||||
6. **InitUser:** `<InitUser />` — Ruft `api.credits.initBalance` beim ersten Login auf
|
||||
|
||||
---
|
||||
|
||||
## Provider-Pattern
|
||||
|
||||
```tsx
|
||||
// components/providers.tsx
|
||||
<ConvexProvider client={convex}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</ConvexProvider>
|
||||
```
|
||||
|
||||
`initialToken` wird server-seitig aus Better Auth ausgelesen und an Convex übergeben — verhindert einen Auth-Flash beim ersten Render. Mit `initialToken` springt `isAuthenticated` sofort auf `true` ohne Loading-Phase.
|
||||
|
||||
---
|
||||
|
||||
## Auth-Flow
|
||||
|
||||
**Better Auth** (self-hosted, open-source). Konfiguration in `lib/auth.ts` (Server) und `lib/auth-client.ts` (Client).
|
||||
|
||||
- Server-Helper: `getAuthUser()`, `getToken()` aus `lib/auth-server.ts`
|
||||
- Client-Helper: `authClient` aus `lib/auth-client.ts`
|
||||
- Convex-Integration: `convex/auth.config.ts` + `convex/auth.ts`
|
||||
- Trusted Origins: `https://app.lemonspace.io`, `http://localhost:3000`
|
||||
|
||||
---
|
||||
|
||||
## Route Groups
|
||||
|
||||
`(app)/` — Klammer-Gruppen teilen kein eigenes Layout-Segment, ermöglichen aber eigene `layout.tsx` für Auth-Guards. Alle eingeloggten Routen liegen hier.
|
||||
|
||||
---
|
||||
|
||||
## API-Routen
|
||||
|
||||
- `api/auth/[...all]` — Better Auth catch-all Handler. Nicht anfassen ohne Better Auth Docs zu lesen.
|
||||
- `api/pexels-video` — Server-seitiger Proxy für Pexels-Video-Anfragen (API-Key bleibt serverseitig).
|
||||
|
||||
---
|
||||
|
||||
## env-Variablen (Next.js-Seite)
|
||||
|
||||
```
|
||||
BETTER_AUTH_SECRET
|
||||
BETTER_AUTH_URL
|
||||
NEXT_PUBLIC_CONVEX_URL
|
||||
```
|
||||
50
components/billing/CLAUDE.md
Normal file
50
components/billing/CLAUDE.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# components/billing/ — Billing & Subscription UI
|
||||
|
||||
UI-Komponenten für Credit-Anzeige, Subscription-Management und Top-Up.
|
||||
|
||||
---
|
||||
|
||||
## Dateien
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `pricing-cards.tsx` | Tier-Vergleich (Free / Starter / Pro / Max) |
|
||||
| `manage-subscription.tsx` | Aktuelle Subscription verwalten (Upgrade, Kündigung) |
|
||||
| `topup-panel.tsx` | Credit-Nachkauf-UI (fixe Pakete + Custom-Betrag) |
|
||||
|
||||
---
|
||||
|
||||
## Tier-Struktur
|
||||
|
||||
Tiers und ihre Credit-Mengen sind in `convex/credits.ts → TIER_CONFIG` definiert. Die Billing-UI liest diese Werte über Convex-Queries — keine doppelte Konfiguration hier.
|
||||
|
||||
```
|
||||
Free: 50 Cr/Monat (€0)
|
||||
Starter: 400 Cr/Monat (€8)
|
||||
Pro: 3.300 Cr/Monat (€59)
|
||||
Max: 6.700 Cr/Monat (€119)
|
||||
```
|
||||
|
||||
**1 Credit = €0,01** — Diese Umrechnung überall konsistent verwenden.
|
||||
|
||||
---
|
||||
|
||||
## Payment-Integration
|
||||
|
||||
**Polar.sh** ist der aktuelle Payment-Provider (MoR, VAT-Handling). Webhooks landen in `convex/http.ts` → `convex/polar.ts`.
|
||||
|
||||
> Hinweis: `convex/schema.ts` enthält noch `lemonSqueezySubscriptionId`-Felder — Überbleibsel einer früheren Integration. Lemon Squeezy ist nicht mehr aktiv. Polar-Felder (`polarSubscriptionId`) sind maßgeblich.
|
||||
|
||||
---
|
||||
|
||||
## Credit-Balance im Dashboard
|
||||
|
||||
Die Credit-Balance wird auch in `components/dashboard/credit-overview.tsx` angezeigt. Beide Komponenten nutzen dieselbe Convex-Query (`api.credits.getBalance`).
|
||||
|
||||
---
|
||||
|
||||
## Konventionen
|
||||
|
||||
- Tier-Upgrades immer über Polar-Checkout (kein direktes Schreiben in `subscriptions`-Tabelle)
|
||||
- `topUp` Mutation (`convex/credits.ts`) für Credit-Nachkauf aufrufen
|
||||
- Monatliches Top-Up-Limit pro Tier beachten (siehe `TIER_CONFIG.topUpLimit`)
|
||||
137
components/canvas/CLAUDE.md
Normal file
137
components/canvas/CLAUDE.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# components/canvas/ — Canvas-Engine
|
||||
|
||||
Der Canvas ist das Herzstück von LemonSpace. Er basiert auf `@xyflow/react` (React Flow) und synchronisiert seinen Zustand bidirektional mit Convex.
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
app/(app)/canvas/[canvasId]/page.tsx
|
||||
└── <Canvas canvasId={...} /> ← components/canvas/canvas.tsx
|
||||
├── <ReactFlowProvider>
|
||||
│ └── <CanvasInner> ← Haupt-Komponente (2800 Zeilen)
|
||||
│ ├── Convex useQuery ← Realtime-Sync
|
||||
│ ├── nodeTypes Map ← node-types.ts
|
||||
│ ├── localStorage Cache ← canvas-local-persistence.ts
|
||||
│ └── Panel-Komponenten
|
||||
└── Context Providers
|
||||
```
|
||||
|
||||
**`canvas.tsx`** ist die zentrale Datei. Sie enthält die gesamte State-Management-Logik, Convex-Mutations, Optimistic Updates und Event-Handler. Sehr groß — vor Änderungen immer den genauen Abschnitt lesen.
|
||||
|
||||
---
|
||||
|
||||
## Convex ↔ React Flow Mapping
|
||||
|
||||
Convex und React Flow verwenden unterschiedliche Datenmodelle. Das Mapping liegt in `lib/canvas-utils.ts`:
|
||||
|
||||
| Richtung | Funktion |
|
||||
|----------|----------|
|
||||
| Convex Node → RF Node | `convexNodeToRF(doc)` |
|
||||
| Convex Edge → RF Edge | `convexEdgeToRF(doc)` |
|
||||
| Edge + Glow | `convexEdgeToRFWithSourceGlow(edge, sourceType, colorMode)` |
|
||||
| StorageId → URL merge | `convexNodeDocWithMergedStorageUrl(node, urlMap, prevMap)` |
|
||||
|
||||
**Wichtig:** Convex speichert `positionX` / `positionY` als separate Felder. React Flow erwartet `position: { x, y }`. Niemals RF-Node-Objekte direkt in Convex schreiben.
|
||||
|
||||
**Status-Injection:** `convexNodeToRF` schreibt `_status`, `_statusMessage` und `retryCount` in `data`, damit Node-Komponenten darauf zugreifen können ohne das Node-Dokument direkt zu kennen.
|
||||
|
||||
**URL-Caching:** Images mit `storageId` werden über einen batch-Storage-URL-Query aufgelöst (`urlByStorage`-Map). Die vorherige URL wird in `previousDataByNodeId` gecacht, um Flackern beim Reload zu vermeiden.
|
||||
|
||||
---
|
||||
|
||||
## Node-Typen (Phase 1)
|
||||
|
||||
Alle registrierten Node-Typen in `node-types.ts`:
|
||||
|
||||
| Typ | Komponente | Kategorie | Handles |
|
||||
|-----|-----------|-----------|---------|
|
||||
| `image` | `ImageNode` | Quelle | source (default), target (default) |
|
||||
| `text` | `TextNode` | Quelle | source (default), target (default) |
|
||||
| `prompt` | `PromptNode` | KI-Ausgabe | source: `prompt-out`, target: `image-in` |
|
||||
| `ai-image` | `AiImageNode` | KI-Ausgabe | source: `image-out`, target: `prompt-in` |
|
||||
| `group` | `GroupNode` | Layout | source (default), target (default) |
|
||||
| `frame` | `FrameNode` | Layout | source: `frame-out`, target: `frame-in` |
|
||||
| `note` | `NoteNode` | Layout | source (default), target (default) |
|
||||
| `compare` | `CompareNode` | Layout | source: `compare-out`, targets: `left`, `right` |
|
||||
| `asset` | `AssetNode` | Quelle | source (default), target (default) |
|
||||
| `video` | `VideoNode` | Quelle | source (default), target (default) |
|
||||
|
||||
> `nodeTypes`-Map **muss** außerhalb jeder React-Komponente definiert sein (sonst re-rendert React Flow bei jedem Render alle Nodes).
|
||||
|
||||
### Default-Größen (`lib/canvas-utils.ts → NODE_DEFAULTS`)
|
||||
|
||||
```
|
||||
image: 280 × 200 prompt: 288 × 220
|
||||
text: 256 × 120 ai-image: 320 × 408
|
||||
group: 400 × 300 frame: 400 × 300
|
||||
note: 208 × 100 compare: 500 × 380
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node-Status-Modell
|
||||
|
||||
```
|
||||
idle → analyzing → clarifying → executing (retry N/2) → done
|
||||
→ error
|
||||
```
|
||||
|
||||
Status + `statusMessage` werden direkt am Node angezeigt. Kein globales Loading-Banner. Bei `error` zeigt `statusMessage` die Kategorie: `Credits: ...`, `Timeout: ...`, `Provider: ...` etc.
|
||||
|
||||
---
|
||||
|
||||
## Edge-Glow-System
|
||||
|
||||
Jede Edge bekommt einen `drop-shadow`-Filter entsprechend dem Quell-Node-Typ. Farben in `lib/canvas-utils.ts → SOURCE_NODE_GLOW_RGB`:
|
||||
|
||||
- `prompt`, `ai-image` → Violett (139, 92, 246)
|
||||
- `image`, `text`, `note` → Teal (13, 148, 136)
|
||||
- `frame` → Orange (249, 115, 22)
|
||||
- `group`, `compare` → Grau (100, 116, 139)
|
||||
|
||||
Compare-Node hat zusätzlich Handle-spezifische Farben (`left` → Blau, `right` → Smaragd).
|
||||
|
||||
---
|
||||
|
||||
## Optimistic Updates & Local Persistence
|
||||
|
||||
**Optimistic Prefix:** Temporäre Nodes/Edges erhalten IDs mit `optimistic_` / `optimistic_edge_`-Prefix. Sie werden durch echte Convex-IDs ersetzt sobald die Mutation abgeschlossen ist.
|
||||
|
||||
**localStorage-Cache** (`lib/canvas-local-persistence.ts`): Snapshot + Ops-Queue pro Canvas-ID.
|
||||
|
||||
- Key-Schema: `lemonspace.canvas:snapshot:v1:<canvasId>` und `lemonspace.canvas:ops:v1:<canvasId>`
|
||||
- Snapshot = letzter bekannter State (Nodes + Edges) für schnellen initialen Render
|
||||
- Ops-Queue = ausstehende Mutations (Recovery bei Verbindungsabbruch, Konzept — noch nicht vollständig implementiert)
|
||||
|
||||
---
|
||||
|
||||
## Panel-Komponenten
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `canvas-toolbar.tsx` | Werkzeug-Leiste (Select, Pan, Zoom-Controls) |
|
||||
| `canvas-app-menu.tsx` | App-Menü (Einstellungen, Logout, Canvas-Name) |
|
||||
| `canvas-sidebar.tsx` | Node-Palette (linke Seite) |
|
||||
| `canvas-command-palette.tsx` | Cmd+K Command Palette |
|
||||
| `canvas-connection-drop-menu.tsx` | Kontext-Menü beim Loslassen einer Verbindung |
|
||||
| `canvas-node-template-picker.tsx` | Node aus Template einfügen |
|
||||
| `canvas-placement-context.tsx` | Context für Drag-and-Drop-Platzierung |
|
||||
| `asset-browser-panel.tsx` | Freepik/Stock-Asset-Browser |
|
||||
| `video-browser-panel.tsx` | Video-Asset-Browser |
|
||||
| `canvas-user-menu.tsx` | User-Avatar und Menü |
|
||||
| `credit-display.tsx` | Credit-Balance Anzeige in der Toolbar |
|
||||
| `export-button.tsx` | Export-Button mit Format-Auswahl |
|
||||
| `connection-banner.tsx` | Offline-Banner bei Convex-Verbindungsverlust |
|
||||
| `custom-connection-line.tsx` | Angepasste temporäre Verbindungslinie |
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Gotchas
|
||||
|
||||
- **`data.url` vs `storageId`:** Node-Komponenten erhalten `data.url` (aufgelöste HTTP-URL), nicht `storageId` direkt. Die URL wird von `convexNodeDocWithMergedStorageUrl` injiziert. Bei neuen Node-Typen mit Bild immer diesen Flow prüfen.
|
||||
- **Min-Zoom:** `CANVAS_MIN_ZOOM = 0.5 / 3` — dreimal weiter raus als React-Flow-Default.
|
||||
- **Parent-Nodes:** `parentId` zeigt auf einen Group- oder Frame-Node. React Flow erwartet, dass Parent-Nodes vor Child-Nodes in der `nodes`-Array stehen.
|
||||
- **Bridge-Edges:** Beim Löschen eines mittleren Nodes werden Kanten automatisch neu verbunden (`computeBridgeCreatesForDeletedNodes` aus `lib/canvas-utils.ts`).
|
||||
- **`"null"`-Handles:** Convex kann `"null"` als String speichern. `convexEdgeToRF` sanitized Handles: `"null"` → `undefined`.
|
||||
39
components/dashboard/CLAUDE.md
Normal file
39
components/dashboard/CLAUDE.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# components/dashboard/ — Dashboard
|
||||
|
||||
UI-Komponenten für die Startseite nach dem Login.
|
||||
|
||||
---
|
||||
|
||||
## Dateien
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `canvas-card.tsx` | Karte für einen Canvas in der Übersicht (Thumbnail, Name, Datum) |
|
||||
| `credit-overview.tsx` | Monatsverbrauch und verfügbare Credits als Balken-Visualisierung |
|
||||
| `recent-transactions.tsx` | Liste der letzten Credit-Transaktionen |
|
||||
|
||||
---
|
||||
|
||||
## Datenquellen
|
||||
|
||||
Alle Daten kommen aus Convex-Queries via `useAuthQuery` (aus `hooks/use-auth-query.ts`):
|
||||
|
||||
| Komponente | Query |
|
||||
|-----------|-------|
|
||||
| `canvas-card.tsx` | `api.canvases.list` |
|
||||
| `credit-overview.tsx` | `api.credits.getBalance`, `api.credits.getUsageStats` |
|
||||
| `recent-transactions.tsx` | `api.credits.getRecentTransactions` |
|
||||
|
||||
---
|
||||
|
||||
## Layout-Seite
|
||||
|
||||
`app/dashboard/page.tsx` — Server Component, rendert Dashboard-Layout mit den Komponenten oben.
|
||||
|
||||
---
|
||||
|
||||
## Konventionen
|
||||
|
||||
- Kein lokaler State für Server-Daten — alles via Convex-Subscriptions (reaktiv)
|
||||
- `useAuthQuery` statt `useQuery` verwenden, um Auth-Races zu vermeiden (skippt Query bis Token bereit ist)
|
||||
- Canvas-Thumbnails sind optional (`thumbnail?: Id<"_storage">`) — Fallback-State immer behandeln
|
||||
51
components/ui/CLAUDE.md
Normal file
51
components/ui/CLAUDE.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# components/ui/ — Design System
|
||||
|
||||
ShadCN-Komponenten mit LemonSpace-spezifischen Anpassungen. Neue UI-Primitive immer hier ablegen.
|
||||
|
||||
---
|
||||
|
||||
## Stack
|
||||
|
||||
- **ShadCN UI** — Komponentenbibliothek (copy-paste, nicht installiertes Paket)
|
||||
- **Tailwind CSS v4** — Utility-first, config in `globals.css` (kein `tailwind.config.ts` in v4)
|
||||
- **Manrope** — Primäre Schrift (`--font-sans`), geladen via `next/font/google`
|
||||
|
||||
---
|
||||
|
||||
## Design-Token-Konventionen
|
||||
|
||||
Tokens folgen dem Manifest: **Teal-Primary, warme Neutraltöne (Sand/Beige), Zitronengelb als Akzent**.
|
||||
|
||||
```css
|
||||
/* Primärfarbe */
|
||||
--primary: teal /* Hauptaktionen, Links, aktive States */
|
||||
|
||||
/* Neutraltöne — immer warm tönen */
|
||||
--background: warm-white /* Nie kaltes #fff oder gray-50 */
|
||||
--muted: sand/beige /* Sekundäre Hintergründe */
|
||||
--border: warm-gray /* Niemals kaltes gray-200 */
|
||||
|
||||
/* Akzentfarbe */
|
||||
--accent: lemon-yellow /* Sparsam einsetzen — Highlights, Badges */
|
||||
```
|
||||
|
||||
**Anti-Pattern:** Kalte Grautöne (`gray-100`, `slate-200`) ohne Wärme-Anpassung. Immer Richtung Sand/Beige tönen.
|
||||
|
||||
---
|
||||
|
||||
## Animationen
|
||||
|
||||
Übergänge mit **exponentiellem Easing** (`ease-out` / `cubic-bezier`). Keine linearen Transitions.
|
||||
|
||||
```css
|
||||
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1); /* expo-out */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Neue Komponenten
|
||||
|
||||
1. ShadCN-Komponente mit `npx shadcn@latest add <name>` hinzufügen
|
||||
2. In `components/ui/` ablegen (automatisch durch ShadCN-CLI)
|
||||
3. Anpassungen direkt in der Komponente — keine separaten Override-Dateien
|
||||
4. Token-Konventionen aus `globals.css` verwenden, keine Hardcoded-Farben
|
||||
123
convex/CLAUDE.md
Normal file
123
convex/CLAUDE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# convex/ — Backend-Schicht
|
||||
|
||||
Convex ist das vollständige Backend von LemonSpace: Datenbank, Realtime-Subscriptions, File Storage und Background-Job-Scheduler in einem. Kein separates API-Layer notwendig.
|
||||
|
||||
---
|
||||
|
||||
## Dateien im Überblick
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `schema.ts` | Einzige Wahrheitsquelle für alle Tabellen und Typen |
|
||||
| `ai.ts` | KI-Bildgenerierungs-Pipeline |
|
||||
| `credits.ts` | Credit-System: Balance, Reservation+Commit, Tier-Config |
|
||||
| `nodes.ts` | CRUD für Canvas-Nodes |
|
||||
| `edges.ts` | CRUD für Canvas-Edges |
|
||||
| `canvases.ts` | CRUD für Canvases |
|
||||
| `openrouter.ts` | OpenRouter-HTTP-Client + Modell-Config (Backend) |
|
||||
| `auth.ts` | Better Auth Integration |
|
||||
| `helpers.ts` | `requireAuth()` — von allen Queries/Mutations genutzt |
|
||||
| `polar.ts` | Polar.sh Webhook-Handler (Subscriptions) |
|
||||
| `pexels.ts` | Pexels Stock-Bilder API |
|
||||
| `freepik.ts` | Freepik Asset-Browser API |
|
||||
| `storage.ts` | Convex File Storage Helpers |
|
||||
| `export.ts` | Canvas-Export-Logik |
|
||||
| `http.ts` | HTTP-Endpunkte (Webhooks) |
|
||||
|
||||
---
|
||||
|
||||
## Schema (`schema.ts`)
|
||||
|
||||
Alle Node-Typen sind in zwei Validators definiert: `phase1NodeTypes` (aktiv) und `nodeType` (alle Phasen). Phase-2/3-Typen werden im Schema vordeklariert, um spätere Schema-Migrationen zu vermeiden — die UI filtert nach Phase.
|
||||
|
||||
### Tabellen
|
||||
|
||||
**`canvases`** — Canvas pro User. Index: `by_owner`, `by_owner_updated`.
|
||||
|
||||
**`nodes`** — Alle Nodes eines Canvas. Felder: `type`, `positionX/Y`, `width`, `height`, `status` (`idle|analyzing|clarifying|executing|done|error`), `statusMessage`, `retryCount`, `data` (v.any()), `parentId`, `zIndex`. Index: `by_canvas`, `by_canvas_type`, `by_parent`.
|
||||
|
||||
> `data` ist `v.any()` — Typ-Safety läuft über den `type`-Discriminator + Zod im Frontend. Die Node-Data-Shapes sind in `schema.ts` dokumentiert (`imageNodeData`, `promptNodeData`, etc.).
|
||||
|
||||
**`edges`** — Verbindungen zwischen Nodes. Index: `by_canvas`, `by_source`, `by_target`.
|
||||
|
||||
**`creditBalances`** — Pro User: `balance`, `reserved`, `monthlyAllocation`. `available = balance - reserved` (computed, nicht gespeichert).
|
||||
|
||||
**`creditTransactions`** — Jede Credit-Bewegung. Types: `subscription | topup | usage | reservation | refund`. Status: `committed | reserved | released | failed`.
|
||||
|
||||
**`subscriptions`** — Aktive Subscription. Tier: `free | starter | pro | max | business`.
|
||||
|
||||
**`dailyUsage`** — Täglicher Zähler pro User für Abuse-Prevention. Key: `userId + date (ISO)`.
|
||||
|
||||
---
|
||||
|
||||
## AI-Pipeline (`ai.ts`)
|
||||
|
||||
```
|
||||
generateImage (action, public)
|
||||
→ checkAbuseLimits (internalMutation)
|
||||
→ reserve (mutation, wenn INTERNAL_CREDITS_ENABLED=true)
|
||||
→ markNodeExecuting (internalMutation)
|
||||
→ scheduler.runAfter(0, processImageGeneration) ← Background-Job
|
||||
|
||||
processImageGeneration (internalAction)
|
||||
→ generateAndStoreImage (internalAction)
|
||||
→ generateImageWithAutoRetry (lokal, max 2 Retries)
|
||||
→ generateImageViaOpenRouter
|
||||
→ ctx.storage.store(blob)
|
||||
→ finalizeImageSuccess (internalMutation)
|
||||
→ commitInternal (credits)
|
||||
[Fehler] → releaseInternal (credits)
|
||||
→ finalizeImageFailure (internalMutation)
|
||||
[finally] → decrementConcurrency
|
||||
```
|
||||
|
||||
**Wichtig:** `generateImage` gibt `{ queued: true }` zurück, sobald der Background-Job geplant ist. Der Node wechselt sofort auf `status: "executing"`. Der eigentliche API-Call läuft asynchron.
|
||||
|
||||
**Retry-Logik:** Max. 2 Retries (`MAX_IMAGE_RETRIES = 2`). Backoff: `min(1500ms, 400ms * retryCount)`. Retryable: `provider` (5xx, 408, 429), `timeout`, `transient`. Nicht retryable: `credits`, `policy`, unbekannte 4xx.
|
||||
|
||||
**env-Flags:**
|
||||
- `INTERNAL_CREDITS_ENABLED=true` — Aktiviert Reservation+Commit. Bei `false` wird nur Abuse-Prevention geprüft.
|
||||
- `OPENROUTER_API_KEY` — Pflicht.
|
||||
|
||||
---
|
||||
|
||||
## Credit-System (`credits.ts`)
|
||||
|
||||
### Tier-Konfiguration (`TIER_CONFIG`)
|
||||
|
||||
| Tier | Credits/Monat | Daily Cap | Concurrency | Premium-Modelle |
|
||||
|------|--------------|-----------|-------------|-----------------|
|
||||
| free | 50 | 10 | 1 | nein |
|
||||
| starter | 400 | 50 | 2 | ja |
|
||||
| pro | 3.300 | 200 | 2 | ja |
|
||||
| max | 6.700 | 500 | 2 | ja |
|
||||
|
||||
**1 Credit = €0,01** (Euro-Cent-Einheit durchgängig in DB und UI).
|
||||
|
||||
### Reservation+Commit-Flow
|
||||
|
||||
1. `reserve()` — Prüft `available >= estimatedCost`, Daily Cap, Concurrency. Erhöht `reserved` + `dailyUsage`. Gibt `transactionId` zurück.
|
||||
2. Job läuft...
|
||||
3. `commitInternal()` — Zieht `actualCost` von `balance` ab, gibt `estimatedCost` aus `reserved` frei. Schreibt `type: "usage"`.
|
||||
4. Bei Fehler: `releaseInternal()` — Gibt `estimatedCost` aus `reserved` frei. `generationCount` bleibt erhöht (Versuch zählt).
|
||||
|
||||
**env-Flag:** `ALLOW_TEST_CREDIT_GRANT=true` — Aktiviert `grantTestCredits` Mutation (nur Dev/Staging).
|
||||
|
||||
---
|
||||
|
||||
## Auth (`helpers.ts`)
|
||||
|
||||
```typescript
|
||||
requireAuth(ctx) // → { userId: string }
|
||||
```
|
||||
|
||||
Wirft bei unauthentifiziertem Zugriff. Wird von allen Queries und Mutations genutzt, die User-Daten berühren.
|
||||
|
||||
---
|
||||
|
||||
## Konventionen
|
||||
|
||||
- `internalMutation` / `internalAction` — Nur von anderen Convex-Funktionen aufrufbar, nicht direkt vom Client.
|
||||
- `mutation` / `query` / `action` — Öffentlich, direkt vom Frontend nutzbar.
|
||||
- Alle User-Daten sind über `userId` (Better Auth User ID, kein Convex-eigenes User-Dokument) indiziert.
|
||||
- Schema-Migrationen: `npx convex dev` erkennt Breaking Changes automatisch. Bei `v.any()`-Feldern gibt es keine automatische Migration — Datensätze müssen manuell/scriptmäßig migriert werden.
|
||||
39
hooks/CLAUDE.md
Normal file
39
hooks/CLAUDE.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# hooks/ — Custom React Hooks
|
||||
|
||||
Geteilte React-Hooks. Nur client-side (`"use client"`).
|
||||
|
||||
---
|
||||
|
||||
## Hooks im Überblick
|
||||
|
||||
### `use-auth-query.ts`
|
||||
|
||||
```typescript
|
||||
useAuthQuery(query, ...args)
|
||||
```
|
||||
|
||||
Wrapper um Convex `useQuery`, der automatisch `"skip"` setzt wenn der Auth-Token noch nicht bereit ist. Verhindert `Unauthenticated`-Fehler bei Queries mit `requireAuth` im Backend.
|
||||
|
||||
**Warum nicht direkt `useQuery`?** Ohne `initialToken` würde Convex kurz eine unauthentifizierte Query feuern und eine Error-Toast anzeigen. Mit `useAuthQuery` wird gewartet bis `isAuthenticated === true`.
|
||||
|
||||
**Wann nutzen:** Immer wenn eine Convex-Query `requireAuth` aufruft. Für öffentliche Queries ist normales `useQuery` in Ordnung.
|
||||
|
||||
---
|
||||
|
||||
### `use-centered-flow-node-position.ts`
|
||||
|
||||
Berechnet die Canvas-Position für einen neuen Node, sodass er im aktuellen Viewport-Zentrum erscheint. Wird beim Einfügen eines Nodes aus der Palette oder Command Palette genutzt.
|
||||
|
||||
---
|
||||
|
||||
### `use-debounced-callback.ts`
|
||||
|
||||
Standard-Debounce-Hook. Wird für teure Operationen wie Canvas-Snapshots und Convex-Mutations beim Resizen/Bewegen von Nodes verwendet.
|
||||
|
||||
---
|
||||
|
||||
## Konventionen
|
||||
|
||||
- Hooks immer mit `use-` Prefix im Dateinamen
|
||||
- Nur wiederverwendbare Hooks hier — canvas-spezifische Inline-Logik bleibt in `canvas.tsx`
|
||||
- Kein direkter Convex-Zugriff in Hooks wenn möglich — Queries/Mutations von der aufrufenden Komponente übergeben lassen
|
||||
107
lib/CLAUDE.md
Normal file
107
lib/CLAUDE.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# lib/ — Utilities & Shared Logic
|
||||
|
||||
Geteilte Hilfsfunktionen, Typ-Definitionen und Konfiguration. Keine React-Komponenten — nur reines TypeScript.
|
||||
|
||||
---
|
||||
|
||||
## Dateien im Überblick
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `canvas-utils.ts` | Convex↔React Flow Adapter, Edge-Glow, Node-Defaults, Bridge-Edges |
|
||||
| `canvas-node-catalog.ts` | Vollständige Node-Taxonomie (alle Phasen, Kategorien, Phase-Flags) |
|
||||
| `canvas-node-templates.ts` | Default-Daten für neue Nodes (beim Einfügen aus Palette) |
|
||||
| `ai-models.ts` | Client-seitige Modell-Definitionen (muss mit `convex/openrouter.ts` in sync bleiben) |
|
||||
| `image-formats.ts` | Aspect-Ratio-Strings, Node-Chrome-Höhen (`AI_IMAGE_NODE_HEADER_PX` etc.) |
|
||||
| `auth.ts` | Better Auth Server-Instanz |
|
||||
| `auth-server.ts` | Server-Helper: `getAuthUser()`, `getToken()` |
|
||||
| `auth-client.ts` | Client-Helper: `authClient` |
|
||||
| `canvas-local-persistence.ts` | localStorage-Cache für Canvas-Snapshots und Op-Queue |
|
||||
| `toast.ts` | Toast-Utility-Wrapper |
|
||||
| `toast-messages.ts` | Typisierte Toast-Message-Definitionen (`msg`, `CanvasNodeDeleteBlockReason`) |
|
||||
| `ai-errors.ts` | Error-Kategorisierung und User-facing Fehlermeldungen |
|
||||
| `pexels-types.ts` | TypeScript-Typen für Pexels-API-Responses |
|
||||
| `polar-products.ts` | Polar.sh Produkt-IDs und Tier-Mapping |
|
||||
| `rate-limit.ts` | Rate-Limiting-Utilities (Redis-backed) |
|
||||
| `redis.ts` | Redis-Client-Initialisierung |
|
||||
| `topup-calculator.ts` | Bonus-Staffel-Berechnung für Credit-Top-Ups |
|
||||
| `format-time.ts` | Zeitformatierung (relative Zeitangaben) |
|
||||
| `utils.ts` | `cn()` (clsx + tailwind-merge), allgemeine Utilities |
|
||||
|
||||
---
|
||||
|
||||
## `canvas-utils.ts` — Wichtigste Datei
|
||||
|
||||
Alle Adapter-Funktionen zwischen Convex-Datenmodell und React Flow. Details in `components/canvas/CLAUDE.md`.
|
||||
|
||||
**Kritische Exports:**
|
||||
- `convexNodeToRF`, `convexEdgeToRF`, `convexEdgeToRFWithSourceGlow`
|
||||
- `convexNodeDocWithMergedStorageUrl` — URL-Injection für Storage-Bilder
|
||||
- `NODE_DEFAULTS` — Default-Größen und Daten per Node-Typ
|
||||
- `NODE_HANDLE_MAP` — Handle-IDs pro Node-Typ
|
||||
- `computeBridgeCreatesForDeletedNodes` — Kanten-Reconnect nach Node-Löschung
|
||||
- `computeMediaNodeSize` — Dynamische Node-Größe basierend auf Bild-Dimensionen
|
||||
|
||||
---
|
||||
|
||||
## `canvas-node-catalog.ts` — Node-Taxonomie
|
||||
|
||||
Einzige Wahrheitsquelle für alle Node-Typen auf Client-Seite.
|
||||
|
||||
```typescript
|
||||
NODE_CATALOG // Alle Nodes aller Phasen
|
||||
NODE_CATEGORY_META // Label + Sortierung pro Kategorie
|
||||
isNodePaletteEnabled // true wenn: implementiert + kein systemOutput + Template vorhanden
|
||||
catalogEntriesByCategory() // Gruppiert für Sidebar-Rendering
|
||||
```
|
||||
|
||||
**Kategorien:** `source`, `ai-output`, `transform`, `image-edit`, `control`, `layout`
|
||||
|
||||
Phase-2/3-Nodes haben `implemented: false` und `disabledHint`. Nie `implemented: true` setzen ohne zugehörige React-Flow-Komponente in `components/canvas/nodes/`.
|
||||
|
||||
---
|
||||
|
||||
## `ai-models.ts` — Sync-Pflicht
|
||||
|
||||
```typescript
|
||||
// Muss identisch zu convex/openrouter.ts sein!
|
||||
export const IMAGE_MODELS: AiModel[]
|
||||
export const DEFAULT_MODEL_ID: string
|
||||
```
|
||||
|
||||
**Achtung:** Diese Datei und `convex/openrouter.ts` müssen immer synchron gehalten werden. Bei neuen Modellen beide Dateien gleichzeitig aktualisieren. `creditCost` muss übereinstimmen — sonst stimmt die angezeigte Kostenvorschau nicht mit dem tatsächlichen Abzug überein.
|
||||
|
||||
---
|
||||
|
||||
## `canvas-local-persistence.ts` — localStorage-Cache
|
||||
|
||||
```typescript
|
||||
readCanvasSnapshot(canvasId) // Letzten Snapshot laden
|
||||
writeCanvasSnapshot(canvasId, {nodes, edges}) // Snapshot speichern
|
||||
enqueueCanvasOp(canvasId, op) // Op in Queue schreiben
|
||||
resolveCanvasOp(canvasId, opId) // Op aus Queue entfernen
|
||||
readCanvasOps(canvasId) // Ausstehende Ops lesen
|
||||
```
|
||||
|
||||
Key-Schema: `lemonspace.canvas:snapshot:v1:<id>` / `lemonspace.canvas:ops:v1:<id>`. Bei Version-Bumps (`SNAPSHOT_VERSION`, `OPS_VERSION`) werden alte Keys automatisch ignoriert.
|
||||
|
||||
---
|
||||
|
||||
## Auth-Helpers
|
||||
|
||||
```typescript
|
||||
// Server (lib/auth-server.ts) — nur in Server Components / Route Handlers
|
||||
getAuthUser() // → Better Auth User | null
|
||||
getToken() // → Convex JWT Token | null (für initialToken im Root Layout)
|
||||
|
||||
// Client (lib/auth-client.ts)
|
||||
authClient // Better Auth Client-Instanz für signIn, signUp, signOut etc.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konventionen
|
||||
|
||||
- Keine React-Imports in `lib/` — reines TypeScript
|
||||
- `utils.ts` für generische Helpers (`cn`, `clamp`, etc.)
|
||||
- Typen, die sowohl Frontend als auch Convex betreffen, gehören in `lib/`, nicht in `convex/`
|
||||
Reference in New Issue
Block a user