Background Job Processor: - Add src/services/jobProcessor.ts that polls RunPod every 30s for stuck jobs - Automatically completes or fails jobs that were abandoned (user navigated away) - Times out jobs after 25 minutes Client-Side Resume: - Add GET /api/generate/pending endpoint to fetch user's processing jobs - Add checkPendingJobs() that runs on login/page load - Show notification banner when user has jobs generating in background - Add "View Progress" button to resume polling for a job Timeout Increases (10min → 25min): - src/utils/validators.ts: request validation max/default - src/config.ts: RUNPOD_MAX_TIMEOUT_MS default - public/js/app.js: client-side polling maxTime - src/services/jobProcessor.ts: background processor timeout CI/CD Optimization: - Add paths-ignore to backend build.yaml to skip rebuilds on frontend-only changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import { existsSync, mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
function requireEnv(name: string): string {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
throw new Error(`Missing required environment variable: ${name}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function optionalEnv(name: string, defaultValue: string): string {
|
|
return process.env[name] || defaultValue;
|
|
}
|
|
|
|
function optionalEnvInt(name: string, defaultValue: number): number {
|
|
const value = process.env[name];
|
|
if (!value) return defaultValue;
|
|
const parsed = parseInt(value, 10);
|
|
if (isNaN(parsed)) return defaultValue;
|
|
return parsed;
|
|
}
|
|
|
|
function optionalEnvBool(name: string, defaultValue: boolean): boolean {
|
|
const value = process.env[name];
|
|
if (!value) return defaultValue;
|
|
return value.toLowerCase() === 'true' || value === '1';
|
|
}
|
|
|
|
const dataDir = optionalEnv('DATA_DIR', './data');
|
|
|
|
// Ensure data directories exist
|
|
if (!existsSync(dataDir)) {
|
|
mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
const contentDir = join(dataDir, 'content');
|
|
if (!existsSync(contentDir)) {
|
|
mkdirSync(contentDir, { recursive: true });
|
|
}
|
|
|
|
export const config = {
|
|
// Server
|
|
nodeEnv: optionalEnv('NODE_ENV', 'development'),
|
|
port: optionalEnvInt('PORT', 3000),
|
|
isProduction: optionalEnv('NODE_ENV', 'development') === 'production',
|
|
|
|
// Paths
|
|
dataDir,
|
|
contentDir,
|
|
dbPath: join(dataDir, 'app.db'),
|
|
|
|
// Session
|
|
sessionSecret: requireEnv('SESSION_SECRET'),
|
|
sessionMaxAge: optionalEnvInt('SESSION_MAX_AGE_HOURS', 24) * 60 * 60 * 1000,
|
|
|
|
// Initial Admin
|
|
adminUsername: optionalEnv('ADMIN_USERNAME', 'admin'),
|
|
adminPassword: optionalEnv('ADMIN_PASSWORD', ''),
|
|
adminEmail: optionalEnv('ADMIN_EMAIL', ''),
|
|
|
|
// RunPod
|
|
runpod: {
|
|
apiKey: requireEnv('RUNPOD_API_KEY'),
|
|
endpointId: requireEnv('RUNPOD_ENDPOINT_ID'),
|
|
baseUrl: 'https://api.runpod.ai/v2',
|
|
pollIntervalMs: optionalEnvInt('RUNPOD_POLL_INTERVAL_MS', 5000),
|
|
maxTimeoutMs: optionalEnvInt('RUNPOD_MAX_TIMEOUT_MS', 1500000),
|
|
},
|
|
|
|
// WebAuthn
|
|
webauthn: {
|
|
rpId: optionalEnv('WEBAUTHN_RP_ID', 'localhost'),
|
|
rpName: optionalEnv('WEBAUTHN_RP_NAME', 'ComfyUI Video Generator'),
|
|
origin: optionalEnv('WEBAUTHN_ORIGIN', 'http://localhost:3000'),
|
|
},
|
|
|
|
// Security
|
|
encryptionKey: requireEnv('ENCRYPTION_KEY'),
|
|
trustProxy: optionalEnvBool('TRUST_PROXY', true),
|
|
|
|
// Rate Limiting
|
|
rateLimit: {
|
|
windowMs: optionalEnvInt('RATE_LIMIT_WINDOW_MS', 60000),
|
|
maxRequests: optionalEnvInt('RATE_LIMIT_MAX_REQUESTS', 100),
|
|
},
|
|
loginRateLimit: {
|
|
windowMs: optionalEnvInt('LOGIN_RATE_LIMIT_WINDOW_MS', 900000),
|
|
maxRequests: optionalEnvInt('LOGIN_RATE_LIMIT_MAX', 5),
|
|
},
|
|
};
|