diff --git a/apps/api/src/modules/exports/routes/admin-export-routes.js b/apps/api/src/modules/exports/routes/admin-export-routes.js index 8688e95..4f49a83 100644 --- a/apps/api/src/modules/exports/routes/admin-export-routes.js +++ b/apps/api/src/modules/exports/routes/admin-export-routes.js @@ -78,4 +78,21 @@ export async function registerAdminExportRoutes(app, options) { }, }); }); + + app.get('/exports/jobs/:id/download', async (request, reply) => { + const job = await adminExportService.getExportJob({ + sellerId: request.admin.sellerId, + jobId: request.params.id, + }); + if (!job) { + throw createApplicationError('EXPORT_JOB_NOT_FOUND', 'Export job was not found.', 404); + } + if (job.status !== 'COMPLETED' || !Array.isArray(job.output)) { + throw createApplicationError('EXPORT_NOT_READY', 'Export is not ready for download.', 409); + } + return reply + .type('application/json; charset=utf-8') + .header('content-disposition', 'attachment; filename="inventory.json"') + .send(JSON.stringify(job.output)); + }); } diff --git a/apps/api/tests/http/admin-export-routes.test.js b/apps/api/tests/http/admin-export-routes.test.js index 41886e2..719cb80 100644 --- a/apps/api/tests/http/admin-export-routes.test.js +++ b/apps/api/tests/http/admin-export-routes.test.js @@ -125,4 +125,9 @@ test('enqueues and polls an asynchronous inventory export', async (testContext) assert.equal(enqueue.json().data.id, 'job-1'); assert.equal(poll.statusCode, 200); assert.deepEqual(poll.json().data.output, [{ id: 'item-1' }]); + + const download = await app.inject({ method: 'GET', url: '/api/v1/admin/exports/jobs/job-1/download' }); + assert.equal(download.statusCode, 200); + assert.equal(download.headers['content-disposition'], 'attachment; filename="inventory.json"'); + assert.deepEqual(JSON.parse(download.body), [{ id: 'item-1' }]); }); diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue index 7dcbac3..9323166 100644 --- a/apps/web/src/pages/AdminHomePage.vue +++ b/apps/web/src/pages/AdminHomePage.vue @@ -55,6 +55,7 @@ const inventory = ref([]); const selectedInventoryIds = ref([]); const inventoryTags = ref(""); const exportStatus = ref(""); +const exportJobId = ref(""); const listings = ref([]); const sales = ref([]); const acquisitions = ref([]); @@ -262,6 +263,7 @@ async function enqueueInventoryExport() { exportStatus.value = "Starting export..."; try { const job = await enqueueAdminInventoryExport(csrfToken()); + exportJobId.value = job.id; const completed = await getAdminInventoryExportJob(job.id); exportStatus.value = `Export ${completed.status.toLowerCase()}.`; } catch (requestError) { @@ -542,6 +544,14 @@ onMounted(load); Queue JSON export {{ exportStatus }} + + Download queued export +
Loading inventory... diff --git a/docs/API.md b/docs/API.md index 0728f02..4d51447 100644 --- a/docs/API.md +++ b/docs/API.md @@ -104,6 +104,7 @@ GET /api/v1/admin/exports/inventory.csv GET /api/v1/admin/exports/inventory.xls POST /api/v1/admin/exports/inventory.json/jobs GET /api/v1/admin/exports/jobs/:id +GET /api/v1/admin/exports/jobs/:id/download GET /api/v1/admin/settings PATCH /api/v1/admin/settings diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index dabebb5..144701e 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -154,6 +154,7 @@ Remaining roadmap areas are bulk status actions, asynchronous large exports, app - Admin inventory Excel-compatible export is implemented at `GET /api/v1/admin/exports/inventory.xls` using the XML workbook format and seller-scoped admin DTOs. - Seller-scoped admin settings are implemented through `GET/PATCH /api/v1/admin/settings` with strict category allowlists and server-side partial-update merging. - Large inventory JSON exports can be enqueued through `POST /api/v1/admin/exports/inventory.json/jobs` and polled through `GET /api/v1/admin/exports/jobs/:id`; the worker owns claim, completion, and failure processing. +- Completed asynchronous inventory JSON exports can be downloaded through `GET /api/v1/admin/exports/jobs/:id/download`. - PostgreSQL restore verification and image snapshot restore scripts are implemented and covered by shell tests as part of the deployment drill workflow. - `npm audit --omit=dev` currently reports zero production vulnerabilities. Re-run the audit after dependency changes and review development-dependency findings separately before production releases.