From 9a6ae3b786afc5686ab69a627f44e77f94afd12e Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sat, 23 May 2026 16:23:23 -0400 Subject: [PATCH] fix(jobs): backfill asset_name from DB so non-YouTube jobs show their asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Jobs screen only displayed an asset name when the enqueueing code stuffed assetName into the BullMQ job data. YouTube imports did that; upload-triggered proxy/thumbnail jobs didn't — so everything except YouTube showed em dashes in the Asset column. Fix it centrally: after we collect jobs from BullMQ, look up names in one bulk SELECT against the assets table for any job that has an assetId but no asset_name. Applies to /jobs, /jobs/:id, and the SSE events stream. Lookup failures fall through silently rather than 500-ing the whole list. Co-Authored-By: Claude Opus 4.7 --- services/mam-api/src/routes/jobs.js | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/services/mam-api/src/routes/jobs.js b/services/mam-api/src/routes/jobs.js index 8227006..a2f8cd0 100644 --- a/services/mam-api/src/routes/jobs.js +++ b/services/mam-api/src/routes/jobs.js @@ -81,6 +81,37 @@ async function getAllBullMQJobs() { return results; } +// Mutate `jobs` in place to fill in asset_name from the assets table for any +// job that has an assetId but no inline assetName in its payload. One bulk +// SQL query per refresh — cheap, and means we don't have to remember to pass +// assetName at every enqueue site (upload.js, capture stop, scheduler, etc.). +async function attachAssetNames(jobs) { + const idsNeedingLookup = [...new Set( + jobs.filter(j => j.asset_id && !j.asset_name).map(j => j.asset_id) + )]; + if (idsNeedingLookup.length === 0) return; + + let rows = []; + try { + const result = await pool.query( + 'SELECT id, display_name, filename FROM assets WHERE id = ANY($1::uuid[])', + [idsNeedingLookup] + ); + rows = result.rows; + } catch { + // If the lookup fails (DB down, bad UUID in a stale BullMQ payload), keep + // serving jobs without names rather than 500-ing the whole list. + return; + } + const byId = new Map(rows.map(r => [r.id, r.display_name || r.filename])); + for (const j of jobs) { + if (j.asset_id && !j.asset_name) { + const name = byId.get(j.asset_id); + if (name) j.asset_name = name; + } + } +} + // ── GET /events – Server-Sent Events stream of live job updates ─────────────── router.get('/events', async (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); @@ -96,6 +127,7 @@ router.get('/events', async (req, res) => { if (closed) return; try { const jobs = await getAllBullMQJobs(); + await attachAssetNames(jobs); if (!closed) res.write(`data: ${JSON.stringify({ type: 'jobs', jobs })}\n\n`); } catch (err) { if (!closed) res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`); @@ -111,6 +143,7 @@ router.get('/', async (req, res, next) => { try { const { type, status, asset_id } = req.query; let jobs = await getAllBullMQJobs(); + await attachAssetNames(jobs); if (type) jobs = jobs.filter(j => j.type === type); if (status) jobs = jobs.filter(j => j.status === status); @@ -138,7 +171,9 @@ router.get('/:id', async (req, res, next) => { if (job) { const state = await job.getState(); const apiStatus = STATE_MAP[state] || state; - return res.json(normalizeJob(job, type, apiStatus)); + const normalized = normalizeJob(job, type, apiStatus); + await attachAssetNames([normalized]); + return res.json(normalized); } } catch { /* try next queue */ } }