Add job logging and increase timeout to 20 minutes
All checks were successful
Build and Push Frontend Docker Image / build (push) Successful in 39s
Build and Push Docker Image / build (push) Successful in 31m7s

- Add JobLogger class to handler.py for structured timestamped logging
- Increase MAX_TIMEOUT from 600s to 1200s (20 minutes)
- Add logs column to generated_content table via migration
- Store and display job execution logs in gallery UI
- Add Logs button to gallery items with modal display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-01-08 02:10:55 +00:00
parent 52dd0d8766
commit 3c421cf7b8
10 changed files with 269 additions and 46 deletions

View File

@@ -50,6 +50,7 @@ function runMigrations(database: Database.Database): void {
// Migration files in order
const migrations = [
{ version: 1, file: '001_initial.sql' },
{ version: 2, file: '002_add_logs.sql' },
];
for (const migration of migrations) {
@@ -131,5 +132,6 @@ export interface GeneratedContentRow {
mime_type: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
error_message: string | null;
logs: string | null;
created_at: string;
}

View File

@@ -0,0 +1,2 @@
-- Add logs column to store job execution logs
ALTER TABLE generated_content ADD COLUMN logs TEXT;

View File

@@ -103,6 +103,35 @@ router.get('/:id', (req, res) => {
});
});
// Get content logs
router.get('/:id/logs', (req, res) => {
const authReq = req as AuthenticatedRequest;
const contentId = parseInt(req.params.id, 10);
if (isNaN(contentId)) {
res.status(400).json({ error: 'Invalid content ID' });
return;
}
const content = getContentById(contentId);
if (!content) {
res.status(404).json({ error: 'Content not found' });
return;
}
if (!canAccessResource(authReq.user, content.userId)) {
res.status(403).json({ error: 'Access denied' });
return;
}
res.json({
contentId: content.id,
status: content.status,
logs: content.logs || [],
errorMessage: content.errorMessage,
});
});
// Download content file
router.get('/:id/download', (req, res) => {
const authReq = req as AuthenticatedRequest;

View File

@@ -123,9 +123,16 @@ router.get('/:jobId/status', asyncHandler(async (req, res) => {
if (output.data) {
// Save base64 data to file
saveContentFile(row.id, output.data);
// Also save logs if present
if (status.output.logs) {
updateContentStatus(row.id, 'completed', { logs: status.output.logs });
}
} else if (output.path) {
// File was saved to volume - update status
updateContentStatus(row.id, 'completed', { fileSize: output.size });
// File was saved to volume - update status with logs
updateContentStatus(row.id, 'completed', {
fileSize: output.size,
logs: status.output.logs,
});
}
}
} else if (status.status === 'FAILED') {
@@ -138,6 +145,7 @@ router.get('/:jobId/status', asyncHandler(async (req, res) => {
if (row) {
updateContentStatus(row.id, 'failed', {
errorMessage: status.error || status.output?.error || 'Unknown error',
logs: status.output?.logs,
});
}
}
@@ -182,6 +190,7 @@ router.get('/content/:contentId/status', (req, res) => {
status: content.status,
runpodJobId: content.runpodJobId,
errorMessage: content.errorMessage,
logs: content.logs,
});
});

View File

@@ -23,6 +23,7 @@ function rowToContent(row: GeneratedContentRow): GeneratedContent {
mimeType: row.mime_type,
status: row.status,
errorMessage: row.error_message,
logs: row.logs ? JSON.parse(row.logs) : null,
createdAt: new Date(row.created_at),
};
}
@@ -68,6 +69,7 @@ export function updateContentStatus(
runpodJobId?: string;
fileSize?: number;
errorMessage?: string;
logs?: string[];
}
): GeneratedContent | null {
const db = getDb();
@@ -87,6 +89,10 @@ export function updateContentStatus(
setParts.push('error_message = ?');
values.push(updates.errorMessage);
}
if (updates?.logs !== undefined) {
setParts.push('logs = ?');
values.push(JSON.stringify(updates.logs));
}
values.push(id);

View File

@@ -58,13 +58,21 @@ async function processStuckJobs(): Promise<void> {
const output = status.output.outputs[0];
if (output.data) {
saveContentFile(job.id, output.data);
// Also save logs if present
if (status.output.logs) {
updateContentStatus(job.id, 'completed', { logs: status.output.logs });
}
} else {
updateContentStatus(job.id, 'completed', { fileSize: output.size });
updateContentStatus(job.id, 'completed', {
fileSize: output.size,
logs: status.output.logs,
});
}
logger.info({ contentId: job.id }, 'Background processor completed job');
} else if (status.status === 'FAILED') {
updateContentStatus(job.id, 'failed', {
errorMessage: status.error || status.output?.error || 'Job failed',
logs: status.output?.logs,
});
logger.info({ contentId: job.id }, 'Background processor marked job as failed');
}

View File

@@ -68,6 +68,7 @@ export interface GeneratedContent {
mimeType: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
errorMessage: string | null;
logs: string[] | null;
createdAt: Date;
}
@@ -99,6 +100,7 @@ export interface RunPodJobStatus extends RunPodJob {
size?: number;
}>;
error?: string;
logs?: string[];
};
error?: string;
}