mirror of
				https://github.com/Wan-Video/Wan2.1.git
				synced 2025-11-03 22:04:21 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			576864de25
			...
			cf5c95fd7a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cf5c95fd7a | 
@ -1,9 +0,0 @@
 | 
				
			|||||||
# Vercel build configuration
 | 
					 | 
				
			||||||
legacy-peer-deps=false
 | 
					 | 
				
			||||||
strict-peer-dependencies=false
 | 
					 | 
				
			||||||
auto-install-peers=true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Performance
 | 
					 | 
				
			||||||
prefer-offline=true
 | 
					 | 
				
			||||||
progress=false
 | 
					 | 
				
			||||||
loglevel=error
 | 
					 | 
				
			||||||
@ -1,519 +0,0 @@
 | 
				
			|||||||
# MCP Server Setup for Wan2.1 PWA
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Overview
 | 
					 | 
				
			||||||
This guide shows how to connect Claude Desktop to your local Wan2.1 PWA project directory for persistent file access and seamless development workflow.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## What is MCP?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Model Context Protocol (MCP)** allows Claude Desktop to directly access and manipulate files in your local project directory, eliminating the need to copy files back and forth.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Benefits
 | 
					 | 
				
			||||||
- 🔄 **Direct File Access** - Claude can read and write files directly in your project
 | 
					 | 
				
			||||||
- 💾 **Persistent Changes** - All edits are saved to your local filesystem
 | 
					 | 
				
			||||||
- 🚀 **Faster Workflow** - No manual copying of code between sessions
 | 
					 | 
				
			||||||
- 🔒 **Secure** - Runs locally on your machine, you control access
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Prerequisites
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- **Claude Desktop** installed ([download here](https://claude.ai/download))
 | 
					 | 
				
			||||||
- **Node.js 18+** installed
 | 
					 | 
				
			||||||
- **Your Wan2.1 PWA project** cloned locally
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Installation & Setup
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Step 1: Install MCP SDK
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Choose one of these methods:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Global Installation (Recommended)
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
npm install -g @modelcontextprotocol/sdk
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Or Use npx (No global install needed)
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
npx @modelcontextprotocol/sdk --version
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Step 2: Get Your Project Path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You need the **absolute path** to your wan-pwa directory.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### macOS/Linux
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
cd /path/to/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
pwd
 | 
					 | 
				
			||||||
# Example output: /Users/yourname/projects/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Windows
 | 
					 | 
				
			||||||
```powershell
 | 
					 | 
				
			||||||
cd C:\path\to\Wan2.1\wan-pwa
 | 
					 | 
				
			||||||
cd
 | 
					 | 
				
			||||||
# Example output: C:\Users\yourname\projects\Wan2.1\wan-pwa
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Copy this path** - you'll need it in the next step.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Step 3: Configure Claude Desktop
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### macOS
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Open Claude Desktop Settings**
 | 
					 | 
				
			||||||
   - Click **Claude** in menu bar → **Settings** (or `Cmd + ,`)
 | 
					 | 
				
			||||||
   - Navigate to **Developer** tab
 | 
					 | 
				
			||||||
   - Click **Edit Config**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. **Add MCP Server Configuration**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Paste this JSON (replace `YOUR_ABSOLUTE_PATH`):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "YOUR_ABSOLUTE_PATH"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   **Example (macOS):**
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "/Users/johnsmith/projects/Wan2.1/wan-pwa"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **Save and Restart**
 | 
					 | 
				
			||||||
   - Save the file (`Cmd + S`)
 | 
					 | 
				
			||||||
   - Quit Claude Desktop (`Cmd + Q`)
 | 
					 | 
				
			||||||
   - Reopen Claude Desktop
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Windows
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Open Claude Desktop Settings**
 | 
					 | 
				
			||||||
   - Click **Settings** icon → **Developer** tab
 | 
					 | 
				
			||||||
   - Click **Edit Config**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. **Add MCP Server Configuration**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Paste this JSON (replace `YOUR_ABSOLUTE_PATH`):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "YOUR_ABSOLUTE_PATH"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   **Example (Windows):**
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "C:\\Users\\johnsmith\\projects\\Wan2.1\\wan-pwa"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   **Note:** Use double backslashes (`\\`) in Windows paths.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **Save and Restart**
 | 
					 | 
				
			||||||
   - Save the file
 | 
					 | 
				
			||||||
   - Close and reopen Claude Desktop
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Linux
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Open Claude Desktop Settings**
 | 
					 | 
				
			||||||
   - Open Settings → Developer tab
 | 
					 | 
				
			||||||
   - Click **Edit Config**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. **Add MCP Server Configuration**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Paste this JSON (replace `YOUR_ABSOLUTE_PATH`):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "YOUR_ABSOLUTE_PATH"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   **Example (Linux):**
 | 
					 | 
				
			||||||
   ```json
 | 
					 | 
				
			||||||
   {
 | 
					 | 
				
			||||||
     "mcpServers": {
 | 
					 | 
				
			||||||
       "wan-pwa": {
 | 
					 | 
				
			||||||
         "command": "npx",
 | 
					 | 
				
			||||||
         "args": [
 | 
					 | 
				
			||||||
           "-y",
 | 
					 | 
				
			||||||
           "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
           "/home/johnsmith/projects/Wan2.1/wan-pwa"
 | 
					 | 
				
			||||||
         ]
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
   }
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **Save and Restart**
 | 
					 | 
				
			||||||
   - Save the file
 | 
					 | 
				
			||||||
   - Restart Claude Desktop
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Step 4: Verify Connection
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Open a new conversation in Claude Desktop and ask:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Can you list the files in my wan-pwa project?
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Expected Response:**
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
wan-pwa/
 | 
					 | 
				
			||||||
├── apps/
 | 
					 | 
				
			||||||
│   ├── web/
 | 
					 | 
				
			||||||
│   └── api/
 | 
					 | 
				
			||||||
├── packages/
 | 
					 | 
				
			||||||
│   ├── db/
 | 
					 | 
				
			||||||
│   └── types/
 | 
					 | 
				
			||||||
├── README.md
 | 
					 | 
				
			||||||
├── SETUP.md
 | 
					 | 
				
			||||||
├── package.json
 | 
					 | 
				
			||||||
...
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
If you see the file structure, **you're connected!** 🎉
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Using MCP with Your Project
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Common Tasks
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### View a File
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Show me the content of apps/web/src/app/page.tsx
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Edit a File
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Update the Button component in apps/web/src/components/ui/button.tsx to add a loading state
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Create New Files
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Create a new API endpoint for video analytics in apps/api/routes/analytics.py
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Run Commands
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Run npm install in the web app directory
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Database Migrations
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Create a new migration to add a 'favorites' table
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Example Workflow
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Ask for Code Review**
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
   Review the generation.py file and suggest improvements
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. **Request New Features**
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
   Add a retry button to failed generations in the history page
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **Debug Issues**
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
   Why is the credit deduction not working? Check the database functions
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
4. **Refactor Code**
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
   Extract the image upload logic into a reusable hook
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Alternative: Claude Code CLI
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For terminal-based workflows:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# Install CLI
 | 
					 | 
				
			||||||
npm install -g claude-code
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Navigate to project
 | 
					 | 
				
			||||||
cd /path/to/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start session
 | 
					 | 
				
			||||||
claude-code
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# In the CLI
 | 
					 | 
				
			||||||
> connect /path/to/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
> list files
 | 
					 | 
				
			||||||
> edit apps/web/src/app/page.tsx
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Troubleshooting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Issue: "MCP server not found"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Solution:**
 | 
					 | 
				
			||||||
1. Verify `@modelcontextprotocol/sdk` is installed:
 | 
					 | 
				
			||||||
   ```bash
 | 
					 | 
				
			||||||
   npm list -g @modelcontextprotocol/sdk
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. Check your config path is absolute (not relative):
 | 
					 | 
				
			||||||
   ```bash
 | 
					 | 
				
			||||||
   # ✅ Correct
 | 
					 | 
				
			||||||
   "/Users/john/projects/Wan2.1/wan-pwa"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   # ❌ Wrong
 | 
					 | 
				
			||||||
   "~/projects/Wan2.1/wan-pwa"
 | 
					 | 
				
			||||||
   "./wan-pwa"
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. Restart Claude Desktop completely
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Issue: "Permission denied"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Solution:**
 | 
					 | 
				
			||||||
1. Check directory permissions:
 | 
					 | 
				
			||||||
   ```bash
 | 
					 | 
				
			||||||
   ls -la /path/to/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. Ensure you have read/write access:
 | 
					 | 
				
			||||||
   ```bash
 | 
					 | 
				
			||||||
   chmod -R u+rw /path/to/Wan2.1/wan-pwa
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. Don't run Claude Desktop with `sudo`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Issue: "Files not updating"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Solution:**
 | 
					 | 
				
			||||||
1. MCP servers don't auto-reload on file changes
 | 
					 | 
				
			||||||
2. Ask Claude to "refresh" or "reload" the file
 | 
					 | 
				
			||||||
3. Restart the MCP server:
 | 
					 | 
				
			||||||
   - Quit Claude Desktop
 | 
					 | 
				
			||||||
   - Reopen and start new conversation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Issue: "command not found: npx"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Solution:**
 | 
					 | 
				
			||||||
1. Install Node.js 18+ from [nodejs.org](https://nodejs.org)
 | 
					 | 
				
			||||||
2. Verify installation:
 | 
					 | 
				
			||||||
   ```bash
 | 
					 | 
				
			||||||
   node --version
 | 
					 | 
				
			||||||
   npm --version
 | 
					 | 
				
			||||||
   npx --version
 | 
					 | 
				
			||||||
   ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. Restart terminal and Claude Desktop
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Issue: Windows Path with Spaces
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Solution:**
 | 
					 | 
				
			||||||
Use double backslashes and quotes:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```json
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "mcpServers": {
 | 
					 | 
				
			||||||
    "wan-pwa": {
 | 
					 | 
				
			||||||
      "command": "npx",
 | 
					 | 
				
			||||||
      "args": [
 | 
					 | 
				
			||||||
        "-y",
 | 
					 | 
				
			||||||
        "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
        "C:\\Users\\John Smith\\projects\\Wan2.1\\wan-pwa"
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Security Best Practices
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### ✅ DO:
 | 
					 | 
				
			||||||
- Only grant access to your project directory
 | 
					 | 
				
			||||||
- Use `.gitignore` for secrets and credentials
 | 
					 | 
				
			||||||
- Keep `.env` files out of version control
 | 
					 | 
				
			||||||
- Review MCP config before saving
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### ❌ DON'T:
 | 
					 | 
				
			||||||
- Grant access to root directory (`/` or `C:\`)
 | 
					 | 
				
			||||||
- Share your Claude Desktop config publicly
 | 
					 | 
				
			||||||
- Commit API keys or secrets to git
 | 
					 | 
				
			||||||
- Run with elevated permissions unnecessarily
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Advanced Configuration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Multiple Projects
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You can configure multiple MCP servers:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```json
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "mcpServers": {
 | 
					 | 
				
			||||||
    "wan-pwa": {
 | 
					 | 
				
			||||||
      "command": "npx",
 | 
					 | 
				
			||||||
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/wan-pwa"]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "other-project": {
 | 
					 | 
				
			||||||
      "command": "npx",
 | 
					 | 
				
			||||||
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/other-project"]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Read-Only Access
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For code review without edit permissions:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```json
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "mcpServers": {
 | 
					 | 
				
			||||||
    "wan-pwa-readonly": {
 | 
					 | 
				
			||||||
      "command": "npx",
 | 
					 | 
				
			||||||
      "args": [
 | 
					 | 
				
			||||||
        "-y",
 | 
					 | 
				
			||||||
        "@modelcontextprotocol/server-filesystem",
 | 
					 | 
				
			||||||
        "/path/to/wan-pwa",
 | 
					 | 
				
			||||||
        "--readonly"
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Comparison: MCP vs Current Session
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| Feature | Current Session | With MCP Server |
 | 
					 | 
				
			||||||
|---------|----------------|-----------------|
 | 
					 | 
				
			||||||
| File Access | Temporary | Persistent |
 | 
					 | 
				
			||||||
| Edits | Need manual copy | Direct to files |
 | 
					 | 
				
			||||||
| Git Integration | Manual commands | Direct access |
 | 
					 | 
				
			||||||
| Multiple Projects | One at a time | Multiple servers |
 | 
					 | 
				
			||||||
| Setup Time | None | 5 minutes |
 | 
					 | 
				
			||||||
| Best For | Quick tasks | Deep development |
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Next Steps
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. ✅ Complete MCP setup using steps above
 | 
					 | 
				
			||||||
2. 🧪 Test connection with simple file operations
 | 
					 | 
				
			||||||
3. 🚀 Start using Claude for development tasks
 | 
					 | 
				
			||||||
4. 📚 Explore [MCP Documentation](https://modelcontextprotocol.io/docs) for advanced features
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Resources
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- **MCP Protocol Documentation**: https://modelcontextprotocol.io/docs
 | 
					 | 
				
			||||||
- **Claude Desktop Download**: https://claude.ai/download
 | 
					 | 
				
			||||||
- **Filesystem Server**: https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
 | 
					 | 
				
			||||||
- **Claude Code Guide**: https://docs.claude.com/en/docs/claude-code
 | 
					 | 
				
			||||||
- **Wan2.1 PWA Docs**: See README.md, SETUP.md, DEPLOYMENT.md
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Support
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Getting Help
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. Check this documentation
 | 
					 | 
				
			||||||
2. Review troubleshooting section
 | 
					 | 
				
			||||||
3. Check [MCP GitHub Issues](https://github.com/modelcontextprotocol/typescript-sdk/issues)
 | 
					 | 
				
			||||||
4. Ask in Claude Desktop (once connected!)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Common Questions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Q: Do I need MCP for development?**
 | 
					 | 
				
			||||||
A: No, but it significantly improves the workflow for active development.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Q: Does MCP work with claude.ai (web)?**
 | 
					 | 
				
			||||||
A: No, MCP is currently desktop-only. Use file uploads for web.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Q: Can I use MCP with VS Code?**
 | 
					 | 
				
			||||||
A: MCP is for Claude Desktop. For VS Code, use the Claude Code extension.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Q: Is my code sent to Anthropic?**
 | 
					 | 
				
			||||||
A: Only the files you discuss in conversations. MCP runs locally.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Q: Can multiple people share an MCP config?**
 | 
					 | 
				
			||||||
A: Yes, but each person needs their own absolute path configured.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Setup Complete?** Start building with Phase 4 features! 🎉
 | 
					 | 
				
			||||||
@ -1,483 +0,0 @@
 | 
				
			|||||||
# Phase 3 Implementation - Backend Integration & Polish
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Overview
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Phase 3 closes the critical integration gaps between the frontend, backend, database, and Replicate API. This document details all implemented changes and how to test them.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## ✅ Completed Features
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 1. Database Integration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Generation records now created BEFORE calling Replicate
 | 
					 | 
				
			||||||
- Credits deducted atomically using database function
 | 
					 | 
				
			||||||
- Job IDs properly tracked for status polling
 | 
					 | 
				
			||||||
- Automatic refunds on failures
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Files Modified:**
 | 
					 | 
				
			||||||
- `packages/db/migrations/002_credit_system.sql` - New migration with credit functions
 | 
					 | 
				
			||||||
- `apps/api/routes/generation.py` - Complete rewrite of generation flow
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**How It Works:**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```python
 | 
					 | 
				
			||||||
# Flow for Text-to-Video generation:
 | 
					 | 
				
			||||||
1. Check user has sufficient credits
 | 
					 | 
				
			||||||
2. Create generation record (status: "queued")
 | 
					 | 
				
			||||||
3. Start Replicate job
 | 
					 | 
				
			||||||
4. Update record with job_id (status: "processing")
 | 
					 | 
				
			||||||
5. Deduct credits using database function
 | 
					 | 
				
			||||||
6. Return generation_id to client
 | 
					 | 
				
			||||||
7. (Webhook) Update record when complete
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Testing:**
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# 1. Run migration in Supabase SQL Editor
 | 
					 | 
				
			||||||
# Copy contents of packages/db/migrations/002_credit_system.sql
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 2. Test credit deduction
 | 
					 | 
				
			||||||
curl -X POST http://localhost:8000/api/generation/text-to-video \
 | 
					 | 
				
			||||||
  -H "Authorization: Bearer YOUR_TOKEN" \
 | 
					 | 
				
			||||||
  -H "Content-Type: application/json" \
 | 
					 | 
				
			||||||
  -d '{
 | 
					 | 
				
			||||||
    "prompt": "Test video",
 | 
					 | 
				
			||||||
    "model": "t2v-14B",
 | 
					 | 
				
			||||||
    "resolution": "720p"
 | 
					 | 
				
			||||||
  }'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 3. Check database
 | 
					 | 
				
			||||||
# generations table should have new record
 | 
					 | 
				
			||||||
# credits should be deducted
 | 
					 | 
				
			||||||
# credit_transactions should have deduction entry
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 2. Webhook Handler
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Created `/api/webhooks/replicate` endpoint
 | 
					 | 
				
			||||||
- HMAC signature verification
 | 
					 | 
				
			||||||
- Automatic status updates from Replicate
 | 
					 | 
				
			||||||
- Refund credits on failures
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Files Created:**
 | 
					 | 
				
			||||||
- `apps/api/routes/webhooks.py` - Webhook handler
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**How It Works:**
 | 
					 | 
				
			||||||
```python
 | 
					 | 
				
			||||||
# When Replicate completes a prediction:
 | 
					 | 
				
			||||||
1. Replicate sends POST to /api/webhooks/replicate
 | 
					 | 
				
			||||||
2. Verify HMAC signature
 | 
					 | 
				
			||||||
3. Find generation by job_id
 | 
					 | 
				
			||||||
4. Update status, progress, video_url
 | 
					 | 
				
			||||||
5. If failed, trigger refund
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Setup:**
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# 1. Deploy API
 | 
					 | 
				
			||||||
modal deploy apps/api/main.py
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 2. Get webhook URL
 | 
					 | 
				
			||||||
# https://your-app--modal.run/api/webhooks/replicate
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 3. Register webhook with Replicate
 | 
					 | 
				
			||||||
curl -X POST https://api.replicate.com/v1/webhooks \
 | 
					 | 
				
			||||||
  -H "Authorization: Token $REPLICATE_API_TOKEN" \
 | 
					 | 
				
			||||||
  -H "Content-Type: application/json" \
 | 
					 | 
				
			||||||
  -d '{
 | 
					 | 
				
			||||||
    "url": "https://your-app--modal.run/api/webhooks/replicate",
 | 
					 | 
				
			||||||
    "events": ["predictions.completed", "predictions.failed"],
 | 
					 | 
				
			||||||
    "secret": "your-webhook-secret"
 | 
					 | 
				
			||||||
  }'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 4. Add secret to environment
 | 
					 | 
				
			||||||
# In Modal: modal secret create wan-secrets REPLICATE_WEBHOOK_SECRET=wh_sec_xxxxx
 | 
					 | 
				
			||||||
# In .env: REPLICATE_WEBHOOK_SECRET=wh_sec_xxxxx
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Testing:**
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# Test webhook endpoint
 | 
					 | 
				
			||||||
curl -X POST http://localhost:8000/api/webhooks/replicate \
 | 
					 | 
				
			||||||
  -H "Content-Type: application/json" \
 | 
					 | 
				
			||||||
  -H "Webhook-Signature: test-signature" \
 | 
					 | 
				
			||||||
  -d '{
 | 
					 | 
				
			||||||
    "id": "test-job-id",
 | 
					 | 
				
			||||||
    "status": "succeeded",
 | 
					 | 
				
			||||||
    "output": "https://example.com/video.mp4"
 | 
					 | 
				
			||||||
  }'
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 3. Credit System Functions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Added `deduct_credits()` - Atomic credit deduction with transaction logging
 | 
					 | 
				
			||||||
- Added `add_credits()` - Add credits with transaction logging
 | 
					 | 
				
			||||||
- Added `refund_credits()` - Automatic refunds for failed generations
 | 
					 | 
				
			||||||
- Added `credit_transactions` table for audit trail
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Database Functions:**
 | 
					 | 
				
			||||||
```sql
 | 
					 | 
				
			||||||
-- Deduct credits (called by API)
 | 
					 | 
				
			||||||
SELECT deduct_credits(
 | 
					 | 
				
			||||||
  'user-uuid',  -- p_user_id
 | 
					 | 
				
			||||||
  20,           -- p_amount
 | 
					 | 
				
			||||||
  'gen-uuid'    -- p_gen_id (optional)
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Add credits (for purchases)
 | 
					 | 
				
			||||||
SELECT add_credits(
 | 
					 | 
				
			||||||
  'user-uuid',     -- p_user_id
 | 
					 | 
				
			||||||
  100,             -- p_amount
 | 
					 | 
				
			||||||
  'purchase',      -- p_type
 | 
					 | 
				
			||||||
  'Bought 100 credits'  -- p_description
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Refund credits (automatic on failure)
 | 
					 | 
				
			||||||
SELECT refund_credits('gen-uuid');
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Testing:**
 | 
					 | 
				
			||||||
```sql
 | 
					 | 
				
			||||||
-- Test deduction
 | 
					 | 
				
			||||||
SELECT deduct_credits('test-user-id', 10, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Verify transaction logged
 | 
					 | 
				
			||||||
SELECT * FROM credit_transactions WHERE user_id = 'test-user-id';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Test refund
 | 
					 | 
				
			||||||
SELECT refund_credits('test-generation-id');
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 4. Frontend Error Handling
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Added `sonner` for toast notifications
 | 
					 | 
				
			||||||
- Created `Providers` component with Toaster
 | 
					 | 
				
			||||||
- Added validation schemas with Zod
 | 
					 | 
				
			||||||
- Created `useCredits` hook for credit management
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Files Created:**
 | 
					 | 
				
			||||||
- `apps/web/src/components/providers.tsx` - Toast provider
 | 
					 | 
				
			||||||
- `apps/web/src/lib/validation/generation.ts` - Zod schemas
 | 
					 | 
				
			||||||
- `apps/web/src/lib/hooks/use-credits.ts` - Credit management hook
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Usage Example:**
 | 
					 | 
				
			||||||
```tsx
 | 
					 | 
				
			||||||
import { toast } from "sonner"
 | 
					 | 
				
			||||||
import { useCredits } from "@/lib/hooks/use-credits"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function GenerationForm() {
 | 
					 | 
				
			||||||
  const { credits, optimisticDeduct } = useCredits(userId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleGenerate = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const response = await fetch('/api/generation/text-to-video', {
 | 
					 | 
				
			||||||
        method: 'POST',
 | 
					 | 
				
			||||||
        body: JSON.stringify(formData)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!response.ok) {
 | 
					 | 
				
			||||||
        const error = await response.json()
 | 
					 | 
				
			||||||
        throw new Error(error.detail)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Optimistically update credits
 | 
					 | 
				
			||||||
      optimisticDeduct(cost)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      toast.success('Generation started!', {
 | 
					 | 
				
			||||||
        description: 'Your video is being generated. Check History for progress.'
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      toast.error('Generation failed', {
 | 
					 | 
				
			||||||
        description: error.message,
 | 
					 | 
				
			||||||
        action: {
 | 
					 | 
				
			||||||
          label: 'Retry',
 | 
					 | 
				
			||||||
          onClick: () => handleGenerate()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 5. Image Upload Component
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Created drag-and-drop image upload
 | 
					 | 
				
			||||||
- Client-side validation (file type, size)
 | 
					 | 
				
			||||||
- Preview functionality
 | 
					 | 
				
			||||||
- Integration ready for I2V
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Files Created:**
 | 
					 | 
				
			||||||
- `apps/web/src/components/generation/image-upload.tsx`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Usage:**
 | 
					 | 
				
			||||||
```tsx
 | 
					 | 
				
			||||||
import { ImageUpload } from "@/components/generation/image-upload"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function I2VForm() {
 | 
					 | 
				
			||||||
  const [inputImage, setInputImage] = useState<File | null>(null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ImageUpload
 | 
					 | 
				
			||||||
      onImageSelect={(file) => setInputImage(file)}
 | 
					 | 
				
			||||||
      onImageRemove={() => setInputImage(null)}
 | 
					 | 
				
			||||||
      maxSizeMB={10}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Testing:**
 | 
					 | 
				
			||||||
1. Drag image file onto upload area
 | 
					 | 
				
			||||||
2. Verify preview shows
 | 
					 | 
				
			||||||
3. Try uploading non-image file (should show error toast)
 | 
					 | 
				
			||||||
4. Try uploading 15MB file (should show size error)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 6. Form Validation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Added Zod schemas for T2V and I2V
 | 
					 | 
				
			||||||
- Validation for prompt length, model selection, resolution
 | 
					 | 
				
			||||||
- Credit cost calculator
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Schemas:**
 | 
					 | 
				
			||||||
```typescript
 | 
					 | 
				
			||||||
import { textToVideoSchema, calculateCreditCost } from '@/lib/validation/generation'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Validate form data
 | 
					 | 
				
			||||||
const result = textToVideoSchema.safeParse(formData)
 | 
					 | 
				
			||||||
if (!result.success) {
 | 
					 | 
				
			||||||
  // Show validation errors
 | 
					 | 
				
			||||||
  console.log(result.error.issues)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Calculate cost
 | 
					 | 
				
			||||||
const cost = calculateCreditCost('t2v-14B', '720p')  // Returns 20
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 7. Settings Page
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**What Changed:**
 | 
					 | 
				
			||||||
- Created basic settings page structure
 | 
					 | 
				
			||||||
- Placeholders for Profile, Billing, API Keys
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Files Created:**
 | 
					 | 
				
			||||||
- `apps/web/src/app/dashboard/settings/page.tsx`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**TODO:**
 | 
					 | 
				
			||||||
- Implement profile editing
 | 
					 | 
				
			||||||
- Add billing/payment integration
 | 
					 | 
				
			||||||
- Create API key management
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🔧 Environment Variables
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Backend (New)
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# Add to apps/api/.env
 | 
					 | 
				
			||||||
REPLICATE_WEBHOOK_SECRET=wh_sec_xxxxxxxxxxxxx
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Frontend (No Changes)
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# Existing .env.local variables still apply
 | 
					 | 
				
			||||||
NEXT_PUBLIC_SUPABASE_URL=...
 | 
					 | 
				
			||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
 | 
					 | 
				
			||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🧪 Testing Checklist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Backend Integration
 | 
					 | 
				
			||||||
- [ ] Create generation → Record appears in database
 | 
					 | 
				
			||||||
- [ ] Credits deduct correctly (20 for 720p, 10 for 480p)
 | 
					 | 
				
			||||||
- [ ] job_id saved to generation record
 | 
					 | 
				
			||||||
- [ ] Status updates via polling work
 | 
					 | 
				
			||||||
- [ ] Webhook updates status automatically
 | 
					 | 
				
			||||||
- [ ] Video URL saved on completion
 | 
					 | 
				
			||||||
- [ ] Failed generations trigger refund
 | 
					 | 
				
			||||||
- [ ] Credit transactions logged correctly
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Frontend
 | 
					 | 
				
			||||||
- [ ] Toast notifications show on success/error
 | 
					 | 
				
			||||||
- [ ] Form validation prevents invalid submissions
 | 
					 | 
				
			||||||
- [ ] Credit balance displays correctly
 | 
					 | 
				
			||||||
- [ ] Low credit warning shows when < 5 credits
 | 
					 | 
				
			||||||
- [ ] Image upload accepts valid files
 | 
					 | 
				
			||||||
- [ ] Image upload rejects invalid files
 | 
					 | 
				
			||||||
- [ ] Settings page loads without errors
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### End-to-End
 | 
					 | 
				
			||||||
- [ ] Sign up → Receive 100 free credits
 | 
					 | 
				
			||||||
- [ ] Generate video → Credits deduct
 | 
					 | 
				
			||||||
- [ ] Poll status → Updates show progress
 | 
					 | 
				
			||||||
- [ ] Video completes → URL available for download
 | 
					 | 
				
			||||||
- [ ] Try with 0 credits → Prevented with error message
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 📊 Database Changes
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### New Table: `credit_transactions`
 | 
					 | 
				
			||||||
```sql
 | 
					 | 
				
			||||||
CREATE TABLE credit_transactions (
 | 
					 | 
				
			||||||
  id UUID PRIMARY KEY,
 | 
					 | 
				
			||||||
  user_id UUID REFERENCES users(id),
 | 
					 | 
				
			||||||
  amount INTEGER NOT NULL,
 | 
					 | 
				
			||||||
  type TEXT NOT NULL,  -- 'deduction', 'purchase', 'refund'
 | 
					 | 
				
			||||||
  generation_id UUID REFERENCES generations(id),
 | 
					 | 
				
			||||||
  description TEXT,
 | 
					 | 
				
			||||||
  created_at TIMESTAMPTZ DEFAULT NOW()
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### New Columns: `generations`
 | 
					 | 
				
			||||||
- `job_id TEXT` - Replicate prediction ID
 | 
					 | 
				
			||||||
- `progress INTEGER` - Progress percentage (0-100)
 | 
					 | 
				
			||||||
- `error_message TEXT` - Error details if failed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### New Functions
 | 
					 | 
				
			||||||
- `deduct_credits(user_id, amount, gen_id)` - Atomic deduction
 | 
					 | 
				
			||||||
- `add_credits(user_id, amount, type, description)` - Add credits
 | 
					 | 
				
			||||||
- `refund_credits(gen_id)` - Refund failed generation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🚀 Deployment Steps
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 1. Database Migration
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# In Supabase SQL Editor:
 | 
					 | 
				
			||||||
# 1. Go to SQL Editor
 | 
					 | 
				
			||||||
# 2. Create new query
 | 
					 | 
				
			||||||
# 3. Paste contents of packages/db/migrations/002_credit_system.sql
 | 
					 | 
				
			||||||
# 4. Run query
 | 
					 | 
				
			||||||
# 5. Verify tables and functions created
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 2. Backend Deployment
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
cd apps/api
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Update environment variables
 | 
					 | 
				
			||||||
# Add REPLICATE_WEBHOOK_SECRET to Modal secrets or .env
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Deploy
 | 
					 | 
				
			||||||
modal deploy main.py
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Note the webhook URL
 | 
					 | 
				
			||||||
# https://your-app--modal.run
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 3. Register Webhook
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
# Set environment variables
 | 
					 | 
				
			||||||
export REPLICATE_API_TOKEN="your-token"
 | 
					 | 
				
			||||||
export WEBHOOK_SECRET="wh_sec_$(openssl rand -hex 32)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Register webhook
 | 
					 | 
				
			||||||
curl -X POST https://api.replicate.com/v1/webhooks \
 | 
					 | 
				
			||||||
  -H "Authorization: Token $REPLICATE_API_TOKEN" \
 | 
					 | 
				
			||||||
  -H "Content-Type: application/json" \
 | 
					 | 
				
			||||||
  -d "{
 | 
					 | 
				
			||||||
    \"url\": \"https://your-app--modal.run/api/webhooks/replicate\",
 | 
					 | 
				
			||||||
    \"events\": [\"predictions.completed\", \"predictions.failed\"],
 | 
					 | 
				
			||||||
    \"secret\": \"$WEBHOOK_SECRET\"
 | 
					 | 
				
			||||||
  }"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Save webhook secret to environment
 | 
					 | 
				
			||||||
# Add REPLICATE_WEBHOOK_SECRET=$WEBHOOK_SECRET to your deployment
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 4. Frontend Deployment
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
cd apps/web
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# No new variables needed
 | 
					 | 
				
			||||||
# Deploy to Vercel
 | 
					 | 
				
			||||||
vercel deploy --prod
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🐛 Known Issues & Limitations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 1. Polling Fallback
 | 
					 | 
				
			||||||
**Issue:** If webhook fails, polling never stops
 | 
					 | 
				
			||||||
**Solution:** Add max polling attempts (implement in Phase 4)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 2. Race Condition
 | 
					 | 
				
			||||||
**Issue:** Multiple concurrent requests could bypass credit check
 | 
					 | 
				
			||||||
**Solution:** Database function ensures atomic operation, but add rate limiting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 3. No Retry Logic
 | 
					 | 
				
			||||||
**Issue:** Failed generations can't be retried
 | 
					 | 
				
			||||||
**Solution:** Add retry button in history (implement in Phase 4)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 4. Storage Costs
 | 
					 | 
				
			||||||
**Issue:** No cleanup of old videos/images
 | 
					 | 
				
			||||||
**Solution:** Implement lifecycle policies (implement in Phase 4)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 5. No Cancel Button
 | 
					 | 
				
			||||||
**Issue:** Users can't stop in-progress generations
 | 
					 | 
				
			||||||
**Solution:** Add cancel endpoint (implement in Phase 4)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 📈 Metrics to Monitor
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Backend
 | 
					 | 
				
			||||||
- Generation success rate (target: > 95%)
 | 
					 | 
				
			||||||
- Average completion time (target: < 5 minutes)
 | 
					 | 
				
			||||||
- Webhook delivery rate (target: > 99%)
 | 
					 | 
				
			||||||
- Credit deduction accuracy (target: 100%)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Frontend
 | 
					 | 
				
			||||||
- Form validation error rate
 | 
					 | 
				
			||||||
- Toast notification engagement
 | 
					 | 
				
			||||||
- Image upload success rate
 | 
					 | 
				
			||||||
- Credit check effectiveness
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🔜 Next Steps (Phase 4)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### High Priority
 | 
					 | 
				
			||||||
1. **Payment Integration** - Stripe for credit purchases
 | 
					 | 
				
			||||||
2. **Retry Logic** - Retry failed generations
 | 
					 | 
				
			||||||
3. **Cancel Function** - Stop in-progress generations
 | 
					 | 
				
			||||||
4. **Video Player** - In-app preview instead of download-only
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Medium Priority
 | 
					 | 
				
			||||||
5. **Batch Operations** - Multi-delete, bulk download
 | 
					 | 
				
			||||||
6. **Admin Panel** - Usage monitoring, user management
 | 
					 | 
				
			||||||
7. **Rate Limiting** - Prevent API abuse
 | 
					 | 
				
			||||||
8. **Caching** - Redis for status queries
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Low Priority
 | 
					 | 
				
			||||||
9. **Analytics** - Track generation patterns
 | 
					 | 
				
			||||||
10. **Social Features** - Share videos, favorites
 | 
					 | 
				
			||||||
11. **Advanced Editing** - VACE integration
 | 
					 | 
				
			||||||
12. **API for Developers** - REST + SDKs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 📚 Additional Resources
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Documentation
 | 
					 | 
				
			||||||
- [Replicate Webhooks](https://replicate.com/docs/webhooks)
 | 
					 | 
				
			||||||
- [Supabase RPC Functions](https://supabase.com/docs/guides/database/functions)
 | 
					 | 
				
			||||||
- [Sonner Toast Library](https://sonner.emilkowal.ski/)
 | 
					 | 
				
			||||||
- [Zod Validation](https://zod.dev/)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Code Examples
 | 
					 | 
				
			||||||
- Database functions: `packages/db/migrations/002_credit_system.sql`
 | 
					 | 
				
			||||||
- Webhook handler: `apps/api/routes/webhooks.py`
 | 
					 | 
				
			||||||
- Credit hook: `apps/web/src/lib/hooks/use-credits.ts`
 | 
					 | 
				
			||||||
- Validation: `apps/web/src/lib/validation/generation.ts`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 🤝 Support
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For issues or questions:
 | 
					 | 
				
			||||||
1. Check this documentation
 | 
					 | 
				
			||||||
2. Review SETUP.md and DEPLOYMENT.md
 | 
					 | 
				
			||||||
3. Check database logs in Supabase
 | 
					 | 
				
			||||||
4. Review API logs in Modal
 | 
					 | 
				
			||||||
5. Open GitHub issue with logs and reproduction steps
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Phase 3 Status:** ✅ Complete
 | 
					 | 
				
			||||||
**Ready for Testing:** Yes
 | 
					 | 
				
			||||||
**Ready for Production:** Pending testing and webhook registration
 | 
					 | 
				
			||||||
@ -4,7 +4,7 @@ from fastapi.responses import JSONResponse
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
from dotenv import load_dotenv
 | 
					from dotenv import load_dotenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from routes import generation, auth, users, webhooks
 | 
					from routes import generation, auth, users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
load_dotenv()
 | 
					load_dotenv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,7 +29,6 @@ app.add_middleware(
 | 
				
			|||||||
app.include_router(generation.router, prefix="/api/generation", tags=["generation"])
 | 
					app.include_router(generation.router, prefix="/api/generation", tags=["generation"])
 | 
				
			||||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
 | 
					app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
 | 
				
			||||||
app.include_router(users.router, prefix="/api/users", tags=["users"])
 | 
					app.include_router(users.router, prefix="/api/users", tags=["users"])
 | 
				
			||||||
app.include_router(webhooks.router, prefix="/api/webhooks", tags=["webhooks"])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.get("/")
 | 
					@app.get("/")
 | 
				
			||||||
 | 
				
			|||||||
@ -34,46 +34,21 @@ async def generate_text_to_video(
 | 
				
			|||||||
    request: TextToVideoRequest, user_id: str = Depends(get_user_id)
 | 
					    request: TextToVideoRequest, user_id: str = Depends(get_user_id)
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Generate video from text prompt"""
 | 
					    """Generate video from text prompt"""
 | 
				
			||||||
    supabase = get_supabase()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Calculate credit cost
 | 
					    # Calculate credit cost
 | 
				
			||||||
    cost = CreditService.calculate_cost(request.model, request.resolution)
 | 
					    cost = CreditService.calculate_cost(request.model, request.resolution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check if user has sufficient credits
 | 
					    # Check and deduct credits
 | 
				
			||||||
    credits_result = await CreditService.get_user_credits(user_id)
 | 
					    has_credits = await CreditService.deduct_credits(
 | 
				
			||||||
    if credits_result < cost:
 | 
					        user_id, cost, f"T2V generation: {request.model} @ {request.resolution}"
 | 
				
			||||||
        raise HTTPException(
 | 
					 | 
				
			||||||
            status_code=402,
 | 
					 | 
				
			||||||
            detail=f"Insufficient credits. You need {cost} credits but have {credits_result}.",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create generation record BEFORE calling Replicate
 | 
					    if not has_credits:
 | 
				
			||||||
    generation_record = (
 | 
					        raise HTTPException(status_code=402, detail="Insufficient credits")
 | 
				
			||||||
        supabase.table("generations")
 | 
					 | 
				
			||||||
        .insert(
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                "user_id": user_id,
 | 
					 | 
				
			||||||
                "type": "text-to-video",
 | 
					 | 
				
			||||||
                "prompt": request.prompt,
 | 
					 | 
				
			||||||
                "negative_prompt": request.negative_prompt,
 | 
					 | 
				
			||||||
                "model": request.model,
 | 
					 | 
				
			||||||
                "resolution": request.resolution,
 | 
					 | 
				
			||||||
                "status": "queued",
 | 
					 | 
				
			||||||
                "credits_used": cost,
 | 
					 | 
				
			||||||
                "progress": 0,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute()
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not generation_record.data:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=500, detail="Failed to create generation record")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    generation_id = generation_record.data[0]["id"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        # Start generation via Replicate
 | 
					        # Start generation via Replicate
 | 
				
			||||||
        job_id = await ReplicateService.generate_text_to_video(
 | 
					        prediction_id = await ReplicateService.generate_text_to_video(
 | 
				
			||||||
            prompt=request.prompt,
 | 
					            prompt=request.prompt,
 | 
				
			||||||
            negative_prompt=request.negative_prompt,
 | 
					            negative_prompt=request.negative_prompt,
 | 
				
			||||||
            model=request.model,
 | 
					            model=request.model,
 | 
				
			||||||
@ -82,43 +57,36 @@ async def generate_text_to_video(
 | 
				
			|||||||
            seed=request.seed,
 | 
					            seed=request.seed,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Update generation with job_id and status
 | 
					        # Store generation record in database
 | 
				
			||||||
        supabase.table("generations").update(
 | 
					        supabase = get_supabase()
 | 
				
			||||||
            {"job_id": job_id, "status": "processing", "progress": 10}
 | 
					        generation = (
 | 
				
			||||||
        ).eq("id", generation_id).execute()
 | 
					            supabase.table("generations")
 | 
				
			||||||
 | 
					            .insert(
 | 
				
			||||||
        # Deduct credits using database function
 | 
					                {
 | 
				
			||||||
        try:
 | 
					                    "id": prediction_id,
 | 
				
			||||||
            supabase.rpc(
 | 
					                    "user_id": user_id,
 | 
				
			||||||
                "deduct_credits", {"p_user_id": user_id, "p_amount": cost, "p_gen_id": generation_id}
 | 
					                    "type": "text-to-video",
 | 
				
			||||||
            ).execute()
 | 
					                    "prompt": request.prompt,
 | 
				
			||||||
        except Exception as credit_error:
 | 
					                    "model": request.model,
 | 
				
			||||||
            # Rollback: delete generation record
 | 
					                    "resolution": request.resolution,
 | 
				
			||||||
            supabase.table("generations").delete().eq("id", generation_id).execute()
 | 
					                    "status": "pending",
 | 
				
			||||||
            raise HTTPException(status_code=402, detail="Failed to deduct credits")
 | 
					                    "credits_used": cost,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GenerationResponse(
 | 
					        return GenerationResponse(
 | 
				
			||||||
            id=generation_id,
 | 
					            id=prediction_id,
 | 
				
			||||||
            status="processing",
 | 
					            status="pending",
 | 
				
			||||||
            created_at=datetime.utcnow(),
 | 
					            created_at=datetime.utcnow(),
 | 
				
			||||||
            credits_used=cost,
 | 
					            credits_used=cost,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except HTTPException:
 | 
					 | 
				
			||||||
        raise
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        # Mark generation as failed
 | 
					        # Refund credits on error
 | 
				
			||||||
        supabase.table("generations").update(
 | 
					        await CreditService.add_credits(user_id, cost, "Refund: Generation failed")
 | 
				
			||||||
            {"status": "failed", "error_message": str(e), "progress": 0}
 | 
					        raise HTTPException(status_code=500, detail=str(e))
 | 
				
			||||||
        ).eq("id", generation_id).execute()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Refund credits if they were deducted
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            supabase.rpc("refund_credits", {"p_gen_id": generation_id}).execute()
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@router.post("/image-to-video", response_model=GenerationResponse)
 | 
					@router.post("/image-to-video", response_model=GenerationResponse)
 | 
				
			||||||
@ -126,47 +94,21 @@ async def generate_image_to_video(
 | 
				
			|||||||
    request: ImageToVideoRequest, user_id: str = Depends(get_user_id)
 | 
					    request: ImageToVideoRequest, user_id: str = Depends(get_user_id)
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    """Generate video from image"""
 | 
					    """Generate video from image"""
 | 
				
			||||||
    supabase = get_supabase()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Calculate credit cost
 | 
					    # Calculate credit cost
 | 
				
			||||||
    cost = CreditService.calculate_cost(request.model, request.resolution)
 | 
					    cost = CreditService.calculate_cost(request.model, request.resolution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check if user has sufficient credits
 | 
					    # Check and deduct credits
 | 
				
			||||||
    credits_result = await CreditService.get_user_credits(user_id)
 | 
					    has_credits = await CreditService.deduct_credits(
 | 
				
			||||||
    if credits_result < cost:
 | 
					        user_id, cost, f"I2V generation: {request.model} @ {request.resolution}"
 | 
				
			||||||
        raise HTTPException(
 | 
					 | 
				
			||||||
            status_code=402,
 | 
					 | 
				
			||||||
            detail=f"Insufficient credits. You need {cost} credits but have {credits_result}.",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create generation record BEFORE calling Replicate
 | 
					    if not has_credits:
 | 
				
			||||||
    generation_record = (
 | 
					        raise HTTPException(status_code=402, detail="Insufficient credits")
 | 
				
			||||||
        supabase.table("generations")
 | 
					 | 
				
			||||||
        .insert(
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                "user_id": user_id,
 | 
					 | 
				
			||||||
                "type": "image-to-video",
 | 
					 | 
				
			||||||
                "prompt": request.prompt,
 | 
					 | 
				
			||||||
                "negative_prompt": request.negative_prompt,
 | 
					 | 
				
			||||||
                "image_url": request.image_url,
 | 
					 | 
				
			||||||
                "model": request.model,
 | 
					 | 
				
			||||||
                "resolution": request.resolution,
 | 
					 | 
				
			||||||
                "status": "queued",
 | 
					 | 
				
			||||||
                "credits_used": cost,
 | 
					 | 
				
			||||||
                "progress": 0,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute()
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not generation_record.data:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=500, detail="Failed to create generation record")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    generation_id = generation_record.data[0]["id"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        # Start generation via Replicate
 | 
					        # Start generation via Replicate
 | 
				
			||||||
        job_id = await ReplicateService.generate_image_to_video(
 | 
					        prediction_id = await ReplicateService.generate_image_to_video(
 | 
				
			||||||
            prompt=request.prompt,
 | 
					            prompt=request.prompt,
 | 
				
			||||||
            image_url=request.image_url,
 | 
					            image_url=request.image_url,
 | 
				
			||||||
            negative_prompt=request.negative_prompt,
 | 
					            negative_prompt=request.negative_prompt,
 | 
				
			||||||
@ -175,43 +117,33 @@ async def generate_image_to_video(
 | 
				
			|||||||
            seed=request.seed,
 | 
					            seed=request.seed,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Update generation with job_id and status
 | 
					        # Store generation record
 | 
				
			||||||
        supabase.table("generations").update(
 | 
					        supabase = get_supabase()
 | 
				
			||||||
            {"job_id": job_id, "status": "processing", "progress": 10}
 | 
					        supabase.table("generations").insert(
 | 
				
			||||||
        ).eq("id", generation_id).execute()
 | 
					            {
 | 
				
			||||||
 | 
					                "id": prediction_id,
 | 
				
			||||||
        # Deduct credits using database function
 | 
					                "user_id": user_id,
 | 
				
			||||||
        try:
 | 
					                "type": "image-to-video",
 | 
				
			||||||
            supabase.rpc(
 | 
					                "prompt": request.prompt,
 | 
				
			||||||
                "deduct_credits", {"p_user_id": user_id, "p_amount": cost, "p_gen_id": generation_id}
 | 
					                "image_url": request.image_url,
 | 
				
			||||||
 | 
					                "model": request.model,
 | 
				
			||||||
 | 
					                "resolution": request.resolution,
 | 
				
			||||||
 | 
					                "status": "pending",
 | 
				
			||||||
 | 
					                "credits_used": cost,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        ).execute()
 | 
					        ).execute()
 | 
				
			||||||
        except Exception as credit_error:
 | 
					 | 
				
			||||||
            # Rollback: delete generation record
 | 
					 | 
				
			||||||
            supabase.table("generations").delete().eq("id", generation_id).execute()
 | 
					 | 
				
			||||||
            raise HTTPException(status_code=402, detail="Failed to deduct credits")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GenerationResponse(
 | 
					        return GenerationResponse(
 | 
				
			||||||
            id=generation_id,
 | 
					            id=prediction_id,
 | 
				
			||||||
            status="processing",
 | 
					            status="pending",
 | 
				
			||||||
            created_at=datetime.utcnow(),
 | 
					            created_at=datetime.utcnow(),
 | 
				
			||||||
            credits_used=cost,
 | 
					            credits_used=cost,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except HTTPException:
 | 
					 | 
				
			||||||
        raise
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        # Mark generation as failed
 | 
					        # Refund credits on error
 | 
				
			||||||
        supabase.table("generations").update(
 | 
					        await CreditService.add_credits(user_id, cost, "Refund: Generation failed")
 | 
				
			||||||
            {"status": "failed", "error_message": str(e), "progress": 0}
 | 
					        raise HTTPException(status_code=500, detail=str(e))
 | 
				
			||||||
        ).eq("id", generation_id).execute()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Refund credits if they were deducted
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            supabase.rpc("refund_credits", {"p_gen_id": generation_id}).execute()
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@router.get("/status/{generation_id}", response_model=GenerationStatus)
 | 
					@router.get("/status/{generation_id}", response_model=GenerationStatus)
 | 
				
			||||||
@ -232,91 +164,28 @@ async def get_generation_status(generation_id: str, user_id: str = Depends(get_u
 | 
				
			|||||||
    if not generation.data:
 | 
					    if not generation.data:
 | 
				
			||||||
        raise HTTPException(status_code=404, detail="Generation not found")
 | 
					        raise HTTPException(status_code=404, detail="Generation not found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    gen_data = generation.data
 | 
					    # Get status from Replicate
 | 
				
			||||||
 | 
					    status = await ReplicateService.get_prediction_status(generation_id)
 | 
				
			||||||
    # If generation is already completed, return cached data
 | 
					 | 
				
			||||||
    if gen_data.get("status") in ["completed", "failed"]:
 | 
					 | 
				
			||||||
        return GenerationStatus(
 | 
					 | 
				
			||||||
            id=generation_id,
 | 
					 | 
				
			||||||
            status=gen_data["status"],
 | 
					 | 
				
			||||||
            progress=gen_data.get("progress", 100 if gen_data["status"] == "completed" else 0),
 | 
					 | 
				
			||||||
            video_url=gen_data.get("video_url"),
 | 
					 | 
				
			||||||
            error=gen_data.get("error_message"),
 | 
					 | 
				
			||||||
            logs=None,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Get live status from Replicate using job_id
 | 
					 | 
				
			||||||
    job_id = gen_data.get("job_id")
 | 
					 | 
				
			||||||
    if not job_id:
 | 
					 | 
				
			||||||
        # No job_id yet, return queued status
 | 
					 | 
				
			||||||
        return GenerationStatus(
 | 
					 | 
				
			||||||
            id=generation_id,
 | 
					 | 
				
			||||||
            status="queued",
 | 
					 | 
				
			||||||
            progress=0,
 | 
					 | 
				
			||||||
            video_url=None,
 | 
					 | 
				
			||||||
            error=None,
 | 
					 | 
				
			||||||
            logs=None,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        replicate_status = await ReplicateService.get_prediction_status(job_id)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Update database with latest status
 | 
					    # Update database with latest status
 | 
				
			||||||
        update_data = {}
 | 
					    update_data = {"status": status["status"]}
 | 
				
			||||||
 | 
					    if status["output"]:
 | 
				
			||||||
 | 
					        update_data["video_url"] = status["output"]
 | 
				
			||||||
 | 
					    if status["error"]:
 | 
				
			||||||
 | 
					        update_data["error"] = status["error"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Map Replicate status to our status
 | 
					 | 
				
			||||||
        status_map = {
 | 
					 | 
				
			||||||
            "starting": "processing",
 | 
					 | 
				
			||||||
            "processing": "processing",
 | 
					 | 
				
			||||||
            "succeeded": "completed",
 | 
					 | 
				
			||||||
            "failed": "failed",
 | 
					 | 
				
			||||||
            "canceled": "failed",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new_status = status_map.get(replicate_status["status"], "processing")
 | 
					 | 
				
			||||||
        update_data["status"] = new_status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Update progress
 | 
					 | 
				
			||||||
        if new_status == "processing":
 | 
					 | 
				
			||||||
            update_data["progress"] = 50
 | 
					 | 
				
			||||||
        elif new_status == "completed":
 | 
					 | 
				
			||||||
            update_data["progress"] = 100
 | 
					 | 
				
			||||||
        elif new_status == "failed":
 | 
					 | 
				
			||||||
            update_data["progress"] = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Save video URL if completed
 | 
					 | 
				
			||||||
        if replicate_status.get("output"):
 | 
					 | 
				
			||||||
            video_url = replicate_status["output"]
 | 
					 | 
				
			||||||
            if isinstance(video_url, list):
 | 
					 | 
				
			||||||
                video_url = video_url[0]
 | 
					 | 
				
			||||||
            update_data["video_url"] = video_url
 | 
					 | 
				
			||||||
            update_data["completed_at"] = datetime.utcnow().isoformat()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Save error if failed
 | 
					 | 
				
			||||||
        if replicate_status.get("error"):
 | 
					 | 
				
			||||||
            update_data["error_message"] = replicate_status["error"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Update database
 | 
					 | 
				
			||||||
    supabase.table("generations").update(update_data).eq("id", generation_id).execute()
 | 
					    supabase.table("generations").update(update_data).eq("id", generation_id).execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GenerationStatus(
 | 
					    # Map status to progress percentage
 | 
				
			||||||
            id=generation_id,
 | 
					    progress_map = {"pending": 0, "processing": 50, "succeeded": 100, "failed": 0}
 | 
				
			||||||
            status=new_status,
 | 
					 | 
				
			||||||
            progress=update_data.get("progress", 0),
 | 
					 | 
				
			||||||
            video_url=update_data.get("video_url"),
 | 
					 | 
				
			||||||
            error=update_data.get("error_message"),
 | 
					 | 
				
			||||||
            logs=replicate_status.get("logs"),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        # If Replicate call fails, return database status
 | 
					 | 
				
			||||||
    return GenerationStatus(
 | 
					    return GenerationStatus(
 | 
				
			||||||
        id=generation_id,
 | 
					        id=generation_id,
 | 
				
			||||||
            status=gen_data["status"],
 | 
					        status=status["status"],
 | 
				
			||||||
            progress=gen_data.get("progress", 0),
 | 
					        progress=progress_map.get(status["status"], 0),
 | 
				
			||||||
            video_url=gen_data.get("video_url"),
 | 
					        video_url=status["output"],
 | 
				
			||||||
            error=gen_data.get("error_message"),
 | 
					        error=status["error"],
 | 
				
			||||||
            logs=None,
 | 
					        logs=status["logs"],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,124 +0,0 @@
 | 
				
			|||||||
from fastapi import APIRouter, HTTPException, Header, Request
 | 
					 | 
				
			||||||
import hmac
 | 
					 | 
				
			||||||
import hashlib
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from core.supabase import get_supabase
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
router = APIRouter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@router.post("/replicate")
 | 
					 | 
				
			||||||
async def replicate_webhook(request: Request, webhook_signature: str = Header(None, alias="Webhook-Signature")):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Handle Replicate completion webhook
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    This endpoint receives push notifications from Replicate when predictions complete,
 | 
					 | 
				
			||||||
    eliminating the need for constant polling.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Read raw body for signature verification
 | 
					 | 
				
			||||||
    body = await request.body()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Verify webhook signature
 | 
					 | 
				
			||||||
    secret = os.getenv("REPLICATE_WEBHOOK_SECRET")
 | 
					 | 
				
			||||||
    if secret:
 | 
					 | 
				
			||||||
        expected_signature = hmac.new(
 | 
					 | 
				
			||||||
            secret.encode(),
 | 
					 | 
				
			||||||
            body,
 | 
					 | 
				
			||||||
            hashlib.sha256
 | 
					 | 
				
			||||||
        ).hexdigest()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not webhook_signature or not hmac.compare_digest(webhook_signature, expected_signature):
 | 
					 | 
				
			||||||
            raise HTTPException(status_code=401, detail="Invalid webhook signature")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Parse payload
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        payload = json.loads(body)
 | 
					 | 
				
			||||||
    except json.JSONDecodeError:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=400, detail="Invalid JSON payload")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Extract prediction data
 | 
					 | 
				
			||||||
    job_id = payload.get("id")
 | 
					 | 
				
			||||||
    status = payload.get("status")
 | 
					 | 
				
			||||||
    output = payload.get("output")
 | 
					 | 
				
			||||||
    error = payload.get("error")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not job_id:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=400, detail="Missing prediction ID")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Update database
 | 
					 | 
				
			||||||
    supabase = get_supabase()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Find generation by job_id
 | 
					 | 
				
			||||||
    generation_result = (
 | 
					 | 
				
			||||||
        supabase.table("generations")
 | 
					 | 
				
			||||||
        .select("id, user_id")
 | 
					 | 
				
			||||||
        .eq("job_id", job_id)
 | 
					 | 
				
			||||||
        .single()
 | 
					 | 
				
			||||||
        .execute()
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not generation_result.data:
 | 
					 | 
				
			||||||
        # Generation not found - this is expected for non-Wan predictions
 | 
					 | 
				
			||||||
        return {"status": "ignored", "message": "Generation not found"}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    generation_id = generation_result.data["id"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Prepare update data
 | 
					 | 
				
			||||||
    update_data = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Map Replicate status to our status
 | 
					 | 
				
			||||||
    status_map = {
 | 
					 | 
				
			||||||
        "starting": "processing",
 | 
					 | 
				
			||||||
        "processing": "processing",
 | 
					 | 
				
			||||||
        "succeeded": "completed",
 | 
					 | 
				
			||||||
        "failed": "failed",
 | 
					 | 
				
			||||||
        "canceled": "failed",
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    new_status = status_map.get(status, "processing")
 | 
					 | 
				
			||||||
    update_data["status"] = new_status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Update progress
 | 
					 | 
				
			||||||
    if new_status == "processing":
 | 
					 | 
				
			||||||
        update_data["progress"] = 50
 | 
					 | 
				
			||||||
    elif new_status == "completed":
 | 
					 | 
				
			||||||
        update_data["progress"] = 100
 | 
					 | 
				
			||||||
        update_data["completed_at"] = datetime.utcnow().isoformat()
 | 
					 | 
				
			||||||
    elif new_status == "failed":
 | 
					 | 
				
			||||||
        update_data["progress"] = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Save video URL if completed
 | 
					 | 
				
			||||||
    if status == "succeeded" and output:
 | 
					 | 
				
			||||||
        video_url = output
 | 
					 | 
				
			||||||
        if isinstance(video_url, list):
 | 
					 | 
				
			||||||
            video_url = video_url[0]
 | 
					 | 
				
			||||||
        update_data["video_url"] = video_url
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Save error if failed
 | 
					 | 
				
			||||||
    if error:
 | 
					 | 
				
			||||||
        update_data["error_message"] = str(error)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Update database
 | 
					 | 
				
			||||||
    supabase.table("generations").update(update_data).eq("id", generation_id).execute()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # If failed, trigger refund
 | 
					 | 
				
			||||||
    if new_status == "failed":
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            supabase.rpc("refund_credits", {"p_gen_id": generation_id}).execute()
 | 
					 | 
				
			||||||
        except Exception as refund_error:
 | 
					 | 
				
			||||||
            print(f"Failed to refund credits for generation {generation_id}: {refund_error}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        "status": "ok",
 | 
					 | 
				
			||||||
        "generation_id": generation_id,
 | 
					 | 
				
			||||||
        "new_status": new_status
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@router.get("/health")
 | 
					 | 
				
			||||||
async def webhook_health():
 | 
					 | 
				
			||||||
    """Health check endpoint for webhook"""
 | 
					 | 
				
			||||||
    return {"status": "ok", "message": "Webhook endpoint is healthy"}
 | 
					 | 
				
			||||||
@ -26,10 +26,9 @@
 | 
				
			|||||||
    "lucide-react": "^0.454.0",
 | 
					    "lucide-react": "^0.454.0",
 | 
				
			||||||
    "next": "15.0.3",
 | 
					    "next": "15.0.3",
 | 
				
			||||||
    "next-pwa": "^5.6.0",
 | 
					    "next-pwa": "^5.6.0",
 | 
				
			||||||
    "react": "^18.2.0",
 | 
					    "react": "^19.0.0-rc.0",
 | 
				
			||||||
    "react-dom": "^18.2.0",
 | 
					    "react-dom": "^19.0.0-rc.0",
 | 
				
			||||||
    "react-hook-form": "^7.53.2",
 | 
					    "react-hook-form": "^7.53.2",
 | 
				
			||||||
    "sonner": "^1.7.1",
 | 
					 | 
				
			||||||
    "tailwind-merge": "^2.5.4",
 | 
					    "tailwind-merge": "^2.5.4",
 | 
				
			||||||
    "tailwindcss-animate": "^1.0.7",
 | 
					    "tailwindcss-animate": "^1.0.7",
 | 
				
			||||||
    "zod": "^3.23.8",
 | 
					    "zod": "^3.23.8",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,44 +0,0 @@
 | 
				
			|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function SettingsPage() {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className="space-y-6">
 | 
					 | 
				
			||||||
      <div>
 | 
					 | 
				
			||||||
        <h1 className="text-3xl font-bold">Settings</h1>
 | 
					 | 
				
			||||||
        <p className="text-muted-foreground">Manage your account and preferences</p>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className="grid gap-6">
 | 
					 | 
				
			||||||
        <Card>
 | 
					 | 
				
			||||||
          <CardHeader>
 | 
					 | 
				
			||||||
            <CardTitle>Profile</CardTitle>
 | 
					 | 
				
			||||||
            <CardDescription>Manage your profile information</CardDescription>
 | 
					 | 
				
			||||||
          </CardHeader>
 | 
					 | 
				
			||||||
          <CardContent>
 | 
					 | 
				
			||||||
            <p className="text-sm text-muted-foreground">Profile settings coming soon...</p>
 | 
					 | 
				
			||||||
          </CardContent>
 | 
					 | 
				
			||||||
        </Card>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <Card>
 | 
					 | 
				
			||||||
          <CardTitle className="p-6 pb-4">Billing & Credits</CardTitle>
 | 
					 | 
				
			||||||
          <CardDescription className="px-6 pb-6">
 | 
					 | 
				
			||||||
            Manage your credits and subscription
 | 
					 | 
				
			||||||
          </CardDescription>
 | 
					 | 
				
			||||||
          <CardContent>
 | 
					 | 
				
			||||||
            <p className="text-sm text-muted-foreground">Billing settings coming soon...</p>
 | 
					 | 
				
			||||||
          </CardContent>
 | 
					 | 
				
			||||||
        </Card>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <Card>
 | 
					 | 
				
			||||||
          <CardHeader>
 | 
					 | 
				
			||||||
            <CardTitle>API Keys</CardTitle>
 | 
					 | 
				
			||||||
            <CardDescription>Manage your API access</CardDescription>
 | 
					 | 
				
			||||||
          </CardHeader>
 | 
					 | 
				
			||||||
          <CardContent>
 | 
					 | 
				
			||||||
            <p className="text-sm text-muted-foreground">API key management coming soon...</p>
 | 
					 | 
				
			||||||
          </CardContent>
 | 
					 | 
				
			||||||
        </Card>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
import type { Metadata } from "next"
 | 
					import type { Metadata } from "next"
 | 
				
			||||||
import { Inter } from "next/font/google"
 | 
					import { Inter } from "next/font/google"
 | 
				
			||||||
import { Providers } from "@/components/providers"
 | 
					 | 
				
			||||||
import "./globals.css"
 | 
					import "./globals.css"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const inter = Inter({ subsets: ["latin"] })
 | 
					const inter = Inter({ subsets: ["latin"] })
 | 
				
			||||||
@ -33,9 +32,7 @@ export default function RootLayout({
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <html lang="en">
 | 
					    <html lang="en">
 | 
				
			||||||
      <body className={inter.className}>
 | 
					      <body className={inter.className}>{children}</body>
 | 
				
			||||||
        <Providers>{children}</Providers>
 | 
					 | 
				
			||||||
      </body>
 | 
					 | 
				
			||||||
    </html>
 | 
					    </html>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,123 +0,0 @@
 | 
				
			|||||||
"use client"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useState, useCallback } from "react"
 | 
					 | 
				
			||||||
import { Upload, X } from "lucide-react"
 | 
					 | 
				
			||||||
import { Button } from "@/components/ui/button"
 | 
					 | 
				
			||||||
import { toast } from "sonner"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ImageUploadProps {
 | 
					 | 
				
			||||||
  onImageSelect: (file: File) => void
 | 
					 | 
				
			||||||
  onImageRemove: () => void
 | 
					 | 
				
			||||||
  maxSizeMB?: number
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ImageUpload({ onImageSelect, onImageRemove, maxSizeMB = 10 }: ImageUploadProps) {
 | 
					 | 
				
			||||||
  const [preview, setPreview] = useState<string | null>(null)
 | 
					 | 
				
			||||||
  const [isDragging, setIsDragging] = useState(false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const validateAndProcessFile = useCallback(
 | 
					 | 
				
			||||||
    (file: File) => {
 | 
					 | 
				
			||||||
      // Validate file type
 | 
					 | 
				
			||||||
      if (!file.type.startsWith("image/")) {
 | 
					 | 
				
			||||||
        toast.error("Invalid file type", {
 | 
					 | 
				
			||||||
          description: "Please upload an image file (PNG, JPG, WEBP)",
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Validate file size
 | 
					 | 
				
			||||||
      const maxSizeBytes = maxSizeMB * 1024 * 1024
 | 
					 | 
				
			||||||
      if (file.size > maxSizeBytes) {
 | 
					 | 
				
			||||||
        toast.error("File too large", {
 | 
					 | 
				
			||||||
          description: `Image must be under ${maxSizeMB}MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Create preview
 | 
					 | 
				
			||||||
      const reader = new FileReader()
 | 
					 | 
				
			||||||
      reader.onload = () => setPreview(reader.result as string)
 | 
					 | 
				
			||||||
      reader.readAsDataURL(file)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      onImageSelect(file)
 | 
					 | 
				
			||||||
      return true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    [maxSizeMB, onImageSelect]
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleDrop = useCallback(
 | 
					 | 
				
			||||||
    (e: React.DragEvent<HTMLDivElement>) => {
 | 
					 | 
				
			||||||
      e.preventDefault()
 | 
					 | 
				
			||||||
      setIsDragging(false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const file = e.dataTransfer.files[0]
 | 
					 | 
				
			||||||
      if (file) {
 | 
					 | 
				
			||||||
        validateAndProcessFile(file)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    [validateAndProcessFile]
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleFileInput = useCallback(
 | 
					 | 
				
			||||||
    (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
					 | 
				
			||||||
      const file = e.target.files?.[0]
 | 
					 | 
				
			||||||
      if (file) {
 | 
					 | 
				
			||||||
        validateAndProcessFile(file)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    [validateAndProcessFile]
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleRemove = () => {
 | 
					 | 
				
			||||||
    setPreview(null)
 | 
					 | 
				
			||||||
    onImageRemove()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (preview) {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className="relative">
 | 
					 | 
				
			||||||
        <img src={preview} alt="Upload preview" className="w-full rounded-lg object-cover" />
 | 
					 | 
				
			||||||
        <Button
 | 
					 | 
				
			||||||
          type="button"
 | 
					 | 
				
			||||||
          variant="destructive"
 | 
					 | 
				
			||||||
          size="icon"
 | 
					 | 
				
			||||||
          className="absolute right-2 top-2"
 | 
					 | 
				
			||||||
          onClick={handleRemove}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <X className="h-4 w-4" />
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      onDrop={handleDrop}
 | 
					 | 
				
			||||||
      onDragOver={(e) => {
 | 
					 | 
				
			||||||
        e.preventDefault()
 | 
					 | 
				
			||||||
        setIsDragging(true)
 | 
					 | 
				
			||||||
      }}
 | 
					 | 
				
			||||||
      onDragLeave={() => setIsDragging(false)}
 | 
					 | 
				
			||||||
      className={`rounded-lg border-2 border-dashed p-8 text-center transition cursor-pointer ${
 | 
					 | 
				
			||||||
        isDragging
 | 
					 | 
				
			||||||
          ? "border-primary bg-primary/5"
 | 
					 | 
				
			||||||
          : "border-muted hover:border-primary/50 hover:bg-muted/50"
 | 
					 | 
				
			||||||
      }`}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <input
 | 
					 | 
				
			||||||
        type="file"
 | 
					 | 
				
			||||||
        accept="image/*"
 | 
					 | 
				
			||||||
        onChange={handleFileInput}
 | 
					 | 
				
			||||||
        className="hidden"
 | 
					 | 
				
			||||||
        id="image-upload"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <label htmlFor="image-upload" className="cursor-pointer">
 | 
					 | 
				
			||||||
        <Upload className="mx-auto mb-4 h-8 w-8 text-muted-foreground" />
 | 
					 | 
				
			||||||
        <p className="font-medium">Drop an image here</p>
 | 
					 | 
				
			||||||
        <p className="mt-1 text-sm text-muted-foreground">
 | 
					 | 
				
			||||||
          or click to browse (PNG, JPG, WEBP • Max {maxSizeMB}MB)
 | 
					 | 
				
			||||||
        </p>
 | 
					 | 
				
			||||||
      </label>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
"use client"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Toaster } from "sonner"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function Providers({ children }: { children: React.ReactNode }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      {children}
 | 
					 | 
				
			||||||
      <Toaster richColors position="top-right" />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,58 +0,0 @@
 | 
				
			|||||||
"use client"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useState, useEffect } from "react"
 | 
					 | 
				
			||||||
import { createClient } from "@/lib/supabase/client"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useCredits(userId: string | undefined) {
 | 
					 | 
				
			||||||
  const [credits, setCredits] = useState<number | null>(null)
 | 
					 | 
				
			||||||
  const [loading, setLoading] = useState(true)
 | 
					 | 
				
			||||||
  const [error, setError] = useState<string | null>(null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchCredits = async () => {
 | 
					 | 
				
			||||||
    if (!userId) {
 | 
					 | 
				
			||||||
      setCredits(null)
 | 
					 | 
				
			||||||
      setLoading(false)
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      setLoading(true)
 | 
					 | 
				
			||||||
      const supabase = createClient()
 | 
					 | 
				
			||||||
      const { data, error: fetchError } = await supabase
 | 
					 | 
				
			||||||
        .from("users")
 | 
					 | 
				
			||||||
        .select("credits")
 | 
					 | 
				
			||||||
        .eq("id", userId)
 | 
					 | 
				
			||||||
        .single()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (fetchError) throw fetchError
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      setCredits(data?.credits || 0)
 | 
					 | 
				
			||||||
      setError(null)
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      setError(err instanceof Error ? err.message : "Failed to fetch credits")
 | 
					 | 
				
			||||||
      setCredits(null)
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setLoading(false)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    fetchCredits()
 | 
					 | 
				
			||||||
  }, [userId])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const refreshCredits = () => {
 | 
					 | 
				
			||||||
    fetchCredits()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const optimisticDeduct = (amount: number) => {
 | 
					 | 
				
			||||||
    setCredits((prev) => (prev !== null ? Math.max(0, prev - amount) : null))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    credits,
 | 
					 | 
				
			||||||
    loading,
 | 
					 | 
				
			||||||
    error,
 | 
					 | 
				
			||||||
    refreshCredits,
 | 
					 | 
				
			||||||
    optimisticDeduct,
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,51 +0,0 @@
 | 
				
			|||||||
import { z } from "zod"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const textToVideoSchema = z.object({
 | 
					 | 
				
			||||||
  prompt: z
 | 
					 | 
				
			||||||
    .string()
 | 
					 | 
				
			||||||
    .min(10, "Prompt must be at least 10 characters")
 | 
					 | 
				
			||||||
    .max(500, "Prompt must be under 500 characters"),
 | 
					 | 
				
			||||||
  negative_prompt: z.string().max(200, "Negative prompt must be under 200 characters").optional(),
 | 
					 | 
				
			||||||
  model: z.enum(["t2v-14B", "t2v-1.3B"], {
 | 
					 | 
				
			||||||
    required_error: "Please select a model",
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  resolution: z.enum(["480p", "720p"], {
 | 
					 | 
				
			||||||
    required_error: "Please select a resolution",
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  duration: z.number().int().min(1).max(10).default(5),
 | 
					 | 
				
			||||||
  seed: z.number().int().optional(),
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const imageToVideoSchema = z.object({
 | 
					 | 
				
			||||||
  prompt: z
 | 
					 | 
				
			||||||
    .string()
 | 
					 | 
				
			||||||
    .min(10, "Prompt must be at least 10 characters")
 | 
					 | 
				
			||||||
    .max(500, "Prompt must be under 500 characters"),
 | 
					 | 
				
			||||||
  negative_prompt: z.string().max(200, "Negative prompt must be under 200 characters").optional(),
 | 
					 | 
				
			||||||
  image_url: z.string().url("Please provide a valid image URL"),
 | 
					 | 
				
			||||||
  model: z.enum(["i2v-14B"], {
 | 
					 | 
				
			||||||
    required_error: "Please select a model",
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  resolution: z.enum(["480p", "720p"], {
 | 
					 | 
				
			||||||
    required_error: "Please select a resolution",
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  duration: z.number().int().min(1).max(10).default(5),
 | 
					 | 
				
			||||||
  seed: z.number().int().optional(),
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type TextToVideoInput = z.infer<typeof textToVideoSchema>
 | 
					 | 
				
			||||||
export type ImageToVideoInput = z.infer<typeof imageToVideoSchema>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Credit cost calculator
 | 
					 | 
				
			||||||
export function calculateCreditCost(model: string, resolution: string): number {
 | 
					 | 
				
			||||||
  const costs: Record<string, number> = {
 | 
					 | 
				
			||||||
    "t2v-14B-720p": 20,
 | 
					 | 
				
			||||||
    "t2v-14B-480p": 10,
 | 
					 | 
				
			||||||
    "t2v-1.3B-480p": 5,
 | 
					 | 
				
			||||||
    "i2v-14B-720p": 25,
 | 
					 | 
				
			||||||
    "i2v-14B-480p": 15,
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const key = `${model}-${resolution}`
 | 
					 | 
				
			||||||
  return costs[key] || 10
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "$schema": "https://openapi.vercel.sh/vercel.json",
 | 
					 | 
				
			||||||
  "buildCommand": "cd ../.. && npm install && npm run build --filter=@wan-pwa/web",
 | 
					 | 
				
			||||||
  "framework": "nextjs",
 | 
					 | 
				
			||||||
  "installCommand": "npm install",
 | 
					 | 
				
			||||||
  "regions": ["iad1"],
 | 
					 | 
				
			||||||
  "env": {
 | 
					 | 
				
			||||||
    "NEXT_PUBLIC_SUPABASE_URL": "@supabase-url",
 | 
					 | 
				
			||||||
    "NEXT_PUBLIC_SUPABASE_ANON_KEY": "@supabase-anon-key",
 | 
					 | 
				
			||||||
    "NEXT_PUBLIC_API_URL": "@api-url",
 | 
					 | 
				
			||||||
    "NEXT_PUBLIC_APP_URL": "@app-url"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,113 +0,0 @@
 | 
				
			|||||||
-- Add credit transaction log (for audit trail)
 | 
					 | 
				
			||||||
CREATE TABLE IF NOT EXISTS public.credit_transactions (
 | 
					 | 
				
			||||||
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
 | 
					 | 
				
			||||||
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
 | 
					 | 
				
			||||||
  amount INTEGER NOT NULL,
 | 
					 | 
				
			||||||
  type TEXT NOT NULL CHECK (type IN ('deduction', 'purchase', 'refund')),
 | 
					 | 
				
			||||||
  generation_id UUID REFERENCES public.generations(id),
 | 
					 | 
				
			||||||
  description TEXT,
 | 
					 | 
				
			||||||
  created_at TIMESTAMPTZ DEFAULT NOW()
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Index for user queries
 | 
					 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_credit_transactions_user ON public.credit_transactions(user_id, created_at DESC);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Enable RLS
 | 
					 | 
				
			||||||
ALTER TABLE public.credit_transactions ENABLE ROW LEVEL SECURITY;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE POLICY "Users can view own transactions"
 | 
					 | 
				
			||||||
  ON public.credit_transactions FOR SELECT
 | 
					 | 
				
			||||||
  USING (auth.uid() = user_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Update deduct_credits function to log transaction
 | 
					 | 
				
			||||||
CREATE OR REPLACE FUNCTION deduct_credits(p_user_id UUID, p_amount INTEGER, p_gen_id UUID DEFAULT NULL)
 | 
					 | 
				
			||||||
RETURNS VOID AS $$
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  -- Deduct credits atomically
 | 
					 | 
				
			||||||
  UPDATE public.users
 | 
					 | 
				
			||||||
  SET credits = credits - p_amount, updated_at = NOW()
 | 
					 | 
				
			||||||
  WHERE id = p_user_id AND credits >= p_amount;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  IF NOT FOUND THEN
 | 
					 | 
				
			||||||
    RAISE EXCEPTION 'Insufficient credits';
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- Log transaction
 | 
					 | 
				
			||||||
  INSERT INTO public.credit_transactions (user_id, amount, type, generation_id, description)
 | 
					 | 
				
			||||||
  VALUES (p_user_id, -p_amount, 'deduction', p_gen_id, 'Video generation');
 | 
					 | 
				
			||||||
END;
 | 
					 | 
				
			||||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Function to add credits (for purchases/refunds)
 | 
					 | 
				
			||||||
CREATE OR REPLACE FUNCTION add_credits(p_user_id UUID, p_amount INTEGER, p_type TEXT, p_description TEXT DEFAULT NULL)
 | 
					 | 
				
			||||||
RETURNS VOID AS $$
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  -- Add credits
 | 
					 | 
				
			||||||
  UPDATE public.users
 | 
					 | 
				
			||||||
  SET credits = credits + p_amount, updated_at = NOW()
 | 
					 | 
				
			||||||
  WHERE id = p_user_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  IF NOT FOUND THEN
 | 
					 | 
				
			||||||
    RAISE EXCEPTION 'User not found';
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- Log transaction
 | 
					 | 
				
			||||||
  INSERT INTO public.credit_transactions (user_id, amount, type, description)
 | 
					 | 
				
			||||||
  VALUES (p_user_id, p_amount, p_type, p_description);
 | 
					 | 
				
			||||||
END;
 | 
					 | 
				
			||||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Function to refund credits
 | 
					 | 
				
			||||||
CREATE OR REPLACE FUNCTION refund_credits(p_gen_id UUID)
 | 
					 | 
				
			||||||
RETURNS VOID AS $$
 | 
					 | 
				
			||||||
DECLARE
 | 
					 | 
				
			||||||
  v_user_id UUID;
 | 
					 | 
				
			||||||
  v_credits_used INTEGER;
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  -- Get generation details
 | 
					 | 
				
			||||||
  SELECT user_id, credits_used INTO v_user_id, v_credits_used
 | 
					 | 
				
			||||||
  FROM public.generations
 | 
					 | 
				
			||||||
  WHERE id = p_gen_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  IF NOT FOUND THEN
 | 
					 | 
				
			||||||
    RAISE EXCEPTION 'Generation not found';
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- Refund credits
 | 
					 | 
				
			||||||
  UPDATE public.users
 | 
					 | 
				
			||||||
  SET credits = credits + v_credits_used, updated_at = NOW()
 | 
					 | 
				
			||||||
  WHERE id = v_user_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- Log refund transaction
 | 
					 | 
				
			||||||
  INSERT INTO public.credit_transactions (user_id, amount, type, generation_id, description)
 | 
					 | 
				
			||||||
  VALUES (v_user_id, v_credits_used, 'refund', p_gen_id, 'Generation failed - refund');
 | 
					 | 
				
			||||||
END;
 | 
					 | 
				
			||||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Add job_id column to generations if not exists
 | 
					 | 
				
			||||||
DO $$
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  IF NOT EXISTS (SELECT 1 FROM information_schema.columns
 | 
					 | 
				
			||||||
                 WHERE table_name='generations' AND column_name='job_id') THEN
 | 
					 | 
				
			||||||
    ALTER TABLE public.generations ADD COLUMN job_id TEXT;
 | 
					 | 
				
			||||||
    CREATE INDEX idx_generations_job_id ON public.generations(job_id);
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
END $$;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Add progress column for tracking
 | 
					 | 
				
			||||||
DO $$
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  IF NOT EXISTS (SELECT 1 FROM information_schema.columns
 | 
					 | 
				
			||||||
                 WHERE table_name='generations' AND column_name='progress') THEN
 | 
					 | 
				
			||||||
    ALTER TABLE public.generations ADD COLUMN progress INTEGER DEFAULT 0;
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
END $$;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- Add error_message column
 | 
					 | 
				
			||||||
DO $$
 | 
					 | 
				
			||||||
BEGIN
 | 
					 | 
				
			||||||
  IF NOT EXISTS (SELECT 1 FROM information_schema.columns
 | 
					 | 
				
			||||||
                 WHERE table_name='generations' AND column_name='error_message') THEN
 | 
					 | 
				
			||||||
    ALTER TABLE public.generations ADD COLUMN error_message TEXT;
 | 
					 | 
				
			||||||
  END IF;
 | 
					 | 
				
			||||||
END $$;
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user