Add job cancellation feature to JobTracker - allows users to cancel queued/processing jobs
This commit is contained in:
parent
700ce92098
commit
87ec0fbe25
4 changed files with 131 additions and 8 deletions
109
backend/scripts/reimport_storage.py
Normal file
109
backend/scripts/reimport_storage.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Re-import existing files from storage directory into database
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import mimetypes
|
||||
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from app.database import SessionLocal
|
||||
from app.models.user import User
|
||||
from app.models.asset import Asset
|
||||
|
||||
def get_file_type(mime_type):
|
||||
"""Determine file type from MIME type"""
|
||||
if not mime_type:
|
||||
return 'other'
|
||||
if mime_type.startswith('image/'):
|
||||
return 'image'
|
||||
if mime_type.startswith('video/'):
|
||||
return 'video'
|
||||
if mime_type.startswith('audio/'):
|
||||
return 'audio'
|
||||
if mime_type.startswith('text/') or 'document' in mime_type:
|
||||
return 'document'
|
||||
return 'other'
|
||||
|
||||
def reimport_files():
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Get or create default user
|
||||
user = db.query(User).filter(User.email == "test@forge.ai").first()
|
||||
if not user:
|
||||
user = User(
|
||||
email="test@forge.ai",
|
||||
name="Test User",
|
||||
is_active=True
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
print(f"✓ Created user: {user.email}")
|
||||
|
||||
storage_path = "/app/storage"
|
||||
imported = 0
|
||||
skipped = 0
|
||||
|
||||
# Scan all subdirectories
|
||||
for subdir in ['images', 'videos', 'audio', 'audios', 'documents']:
|
||||
dir_path = os.path.join(storage_path, subdir)
|
||||
if not os.path.exists(dir_path):
|
||||
continue
|
||||
|
||||
print(f"\n📁 Scanning {subdir}/...")
|
||||
|
||||
for filename in os.listdir(dir_path):
|
||||
file_path = os.path.join(dir_path, filename)
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
continue
|
||||
|
||||
# Check if already exists
|
||||
existing = db.query(Asset).filter(Asset.file_path == file_path).first()
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Get file info
|
||||
file_stat = os.stat(file_path)
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
file_type = get_file_type(mime_type)
|
||||
|
||||
# Create asset record
|
||||
asset = Asset(
|
||||
user_id=user.id,
|
||||
original_filename=filename,
|
||||
stored_filename=filename,
|
||||
file_path=file_path,
|
||||
file_type=file_type,
|
||||
mime_type=mime_type or 'application/octet-stream',
|
||||
created_at=datetime.fromtimestamp(file_stat.st_ctime)
|
||||
)
|
||||
|
||||
db.add(asset)
|
||||
imported += 1
|
||||
|
||||
if imported % 50 == 0:
|
||||
db.commit()
|
||||
print(f" Imported {imported} files...")
|
||||
|
||||
db.commit()
|
||||
|
||||
print(f"\n✅ Import complete!")
|
||||
print(f" Imported: {imported} files")
|
||||
print(f" Skipped: {skipped} files (already in database)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reimport_files()
|
||||
|
|
@ -208,7 +208,7 @@ export default function NanoBananaProPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6 md:p-12 font-sans">
|
||||
<div className="min-h-screen bg-black text-white p-6 md:p-12 font-montserrat">
|
||||
<div className="max-w-[1600px] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
||||
{/* Header */}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default function VideoGeneratePage() {
|
|||
}
|
||||
|
||||
// Model-specific controls
|
||||
const modelConfig = config.models?.find(m => m.id === config.defaultModel);
|
||||
const modelConfig = config.models?.find((m: any) => m.id === config.defaultModel);
|
||||
if (modelConfig?.controls && Array.isArray(modelConfig.controls)) {
|
||||
modelConfig.controls.forEach((control) => {
|
||||
defaults[control.name] = control.default;
|
||||
|
|
@ -143,7 +143,7 @@ export default function VideoGeneratePage() {
|
|||
|
||||
if (!capabilities) return;
|
||||
const config = capabilities[provider];
|
||||
const modelConfig = config.models.find(m => m.id === newModel);
|
||||
const modelConfig = config.models.find((m: any) => m.id === newModel);
|
||||
|
||||
// Merge current options with model defaults
|
||||
const modelDefaults: Record<string, any> = {};
|
||||
|
|
@ -549,7 +549,7 @@ export default function VideoGeneratePage() {
|
|||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => handleModelChange(e.target.value)}
|
||||
className="select-field"
|
||||
>
|
||||
{currentConfig?.models.map((m) => (
|
||||
{currentConfig?.models.map((m: any) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
|
|
@ -559,9 +559,9 @@ export default function VideoGeneratePage() {
|
|||
</div>
|
||||
|
||||
{/* Model Description */}
|
||||
{currentConfig?.models.find(m => m.id === model)?.description && (
|
||||
{currentConfig?.models.find((m: any) => m.id === model)?.description && (
|
||||
<p className="text-xs text-gray-500 -mt-4">
|
||||
{currentConfig.models.find(m => m.id === model)?.description}
|
||||
{currentConfig.models.find((m: any) => m.id === model)?.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,8 +216,22 @@ export default function JobTracker({ className }: JobTrackerProps) {
|
|||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(job.status)}
|
||||
<button
|
||||
onClick={() => removeJob(job.id)}
|
||||
className="p-1 text-gray-500 hover:text-gray-300"
|
||||
onClick={async () => {
|
||||
// If job is still running, cancel it in the backend
|
||||
if (job.status === 'queued' || job.status === 'processing') {
|
||||
try {
|
||||
await api.delete(`/jobs/${job.id}`);
|
||||
toast.success('Job cancelled');
|
||||
} catch (err) {
|
||||
console.error('Failed to cancel job:', err);
|
||||
toast.error('Failed to cancel job');
|
||||
}
|
||||
}
|
||||
// Remove from UI
|
||||
removeJob(job.id);
|
||||
}}
|
||||
className="p-1 text-gray-500 hover:text-red-400 transition-colors"
|
||||
title={job.status === 'queued' || job.status === 'processing' ? 'Cancel job' : 'Remove from list'}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue