From ec322df25c21f66ca0e5e4aba1ed80b0d27a51f2 Mon Sep 17 00:00:00 2001
From: FragginWagon
Date: Thu, 10 Sep 2026 01:31:09 -0400
Subject: [PATCH] feat: download completed export jobs
---
.../exports/routes/admin-export-routes.js | 17 +++++++++++++++++
apps/api/tests/http/admin-export-routes.test.js | 5 +++++
apps/web/src/pages/AdminHomePage.vue | 10 ++++++++++
docs/API.md | 1 +
docs/IMPLEMENTATION-STATUS.md | 1 +
5 files changed, 34 insertions(+)
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.