feat(assets): add POST /:id/retry to re-queue errored assets

Assets stuck in status='error' had no recovery path without manual DB
edits. Adds a retry endpoint that re-dispatches the proxy job, which
chains into thumbnail generation automatically and restores the asset
to 'processing' → 'ready' without operator intervention.
This commit is contained in:
Zac Gaetano 2026-05-19 00:17:00 -04:00
parent 2bb731c7fc
commit d3e12deb18

View file

@ -15,6 +15,10 @@ const parseRedisUrl = (url) => {
return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
};
const proxyQueue = new Queue('proxy', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
});
const thumbnailQueue = new Queue('thumbnail', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
});
@ -324,6 +328,46 @@ router.post('/:id/copy', async (req, res, next) => {
}
});
// POST /:id/retry - Re-queue proxy generation for an asset stuck in error state
//
// Proxy failures leave assets at status='error' with no recovery path from the
// UI. This endpoint re-dispatches the proxy job so the worker chain
// (proxy → thumbnail) runs again without manual DB edits.
router.post('/:id/retry', async (req, res, next) => {
try {
const { id } = req.params;
const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
const asset = r.rows[0];
if (asset.status !== 'error') {
return res.status(400).json({ error: `Asset is not in error state (current: ${asset.status})` });
}
if (!asset.original_s3_key) {
return res.status(400).json({ error: 'Asset has no source file to reprocess' });
}
// Re-use the existing proxy key if one was partially written; otherwise
// construct the canonical key so the worker chain writes to the right place.
const proxyKey = asset.proxy_s3_key || `proxies/${id}.mp4`;
await proxyQueue.add('generate', {
assetId: id,
inputKey: asset.original_s3_key,
outputKey: proxyKey,
});
const updated = await pool.query(
`UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1 RETURNING *`,
[id]
);
res.json(updated.rows[0]);
} catch (err) {
next(err);
}
});
// DELETE /:id - Soft or hard delete
router.delete('/:id', async (req, res, next) => {
try {