Add job cancellation feature to JobTracker - allows users to cancel queued/processing jobs

This commit is contained in:
DJP 2025-12-13 15:40:01 -05:00
parent 700ce92098
commit 87ec0fbe25
4 changed files with 131 additions and 8 deletions

View 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()

View file

@ -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 */}

View file

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

View file

@ -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>