- Add jobStore.js: in-memory job store with rich job objects (liveStats, logs, errors, cancellation, timing, success rate) - Rewrite manageProducts.js: structured logging ([STATS], [PRODUCT-OK], [PRODUCT-FAIL], [SKIP], [FETCH], [CANCEL], etc.), per-product cancel checks, jobStore integration - server.js: expose GET /health, GET /jobs, GET /jobs/:id, POST /jobs/:id/cancel, GET /shops endpoints - tokenStore.js: add listTokens() export Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
3.6 KiB
JavaScript
157 lines
3.6 KiB
JavaScript
// jobStore.js — in-memory job tracker for product import jobs
|
|
const { v4: uuid } = require('uuid');
|
|
|
|
const jobs = {};
|
|
|
|
const MAX_LOGS = 120;
|
|
const MAX_ERRORS = 30;
|
|
const MAX_RESULTS = 100;
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function elapsedSeconds(startedAt) {
|
|
if (!startedAt) return 0;
|
|
return Number(((Date.now() - new Date(startedAt).getTime()) / 1000).toFixed(1));
|
|
}
|
|
|
|
function createJob({ shop, brandId, brandName, totalSelected = 0 }) {
|
|
const id = uuid();
|
|
const job = {
|
|
id,
|
|
shop,
|
|
brandId: String(brandId || ''),
|
|
brandName: brandName || '',
|
|
status: 'started',
|
|
step: 'started',
|
|
detail: 'Initialising import job...',
|
|
currentProduct: null,
|
|
liveStats: {
|
|
total: totalSelected,
|
|
processed: 0,
|
|
created: 0,
|
|
skipped: 0,
|
|
failed: 0,
|
|
remaining: totalSelected,
|
|
successRate: 0,
|
|
label: `0/${totalSelected}`,
|
|
},
|
|
errors: [],
|
|
logs: [],
|
|
results: [],
|
|
startedAt: nowIso(),
|
|
updatedAt: nowIso(),
|
|
finishedAt: null,
|
|
durationSeconds: null,
|
|
cancelled: false,
|
|
};
|
|
jobs[id] = job;
|
|
return job;
|
|
}
|
|
|
|
function updateJob(jobId, patch) {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
Object.assign(job, patch, { updatedAt: nowIso() });
|
|
// Recompute derived stats whenever liveStats is patched
|
|
if (patch.liveStats) {
|
|
const s = job.liveStats;
|
|
s.remaining = Math.max(0, s.total - s.processed);
|
|
s.successRate = s.processed > 0
|
|
? Number((((s.processed - s.failed) / s.processed) * 100).toFixed(1))
|
|
: 0;
|
|
s.label = `${s.processed}/${s.total}`;
|
|
}
|
|
return job;
|
|
}
|
|
|
|
function appendJobLog(jobId, line, extraPatch = {}) {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
job.logs.push({ at: nowIso(), line });
|
|
if (job.logs.length > MAX_LOGS) job.logs.shift();
|
|
Object.assign(job, extraPatch, { updatedAt: nowIso() });
|
|
return job;
|
|
}
|
|
|
|
function recordProductResult(jobId, result) {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
job.results.push(result);
|
|
if (job.results.length > MAX_RESULTS) job.results.shift();
|
|
job.updatedAt = nowIso();
|
|
return job;
|
|
}
|
|
|
|
function recordProductError(jobId, errorEntry) {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
job.errors.push(errorEntry);
|
|
if (job.errors.length > MAX_ERRORS) job.errors.shift();
|
|
job.updatedAt = nowIso();
|
|
return job;
|
|
}
|
|
|
|
function finishJob(jobId, status = 'done') {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
const finishedAt = nowIso();
|
|
const durationSeconds = elapsedSeconds(job.startedAt);
|
|
const s = job.liveStats;
|
|
Object.assign(job, {
|
|
status,
|
|
step: status === 'done' ? 'completed' : status,
|
|
finishedAt,
|
|
durationSeconds,
|
|
updatedAt: finishedAt,
|
|
});
|
|
s.remaining = 0;
|
|
s.successRate = s.processed > 0
|
|
? Number((((s.processed - s.failed) / s.processed) * 100).toFixed(1))
|
|
: 0;
|
|
return job;
|
|
}
|
|
|
|
function cancelJob(jobId) {
|
|
const job = jobs[jobId];
|
|
if (!job) return null;
|
|
job.cancelled = true;
|
|
job.status = 'cancelling';
|
|
job.updatedAt = nowIso();
|
|
return job;
|
|
}
|
|
|
|
function isJobCancelled(jobId) {
|
|
return !!(jobs[jobId]?.cancelled);
|
|
}
|
|
|
|
function getJob(jobId) {
|
|
return jobs[jobId] || null;
|
|
}
|
|
|
|
function listJobs(shop = null) {
|
|
const all = Object.values(jobs);
|
|
const filtered = shop ? all.filter(j => j.shop === shop) : all;
|
|
return filtered.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
|
|
}
|
|
|
|
function getLatestJobForShop(shop) {
|
|
const shopJobs = listJobs(shop);
|
|
return shopJobs[0] || null;
|
|
}
|
|
|
|
module.exports = {
|
|
createJob,
|
|
updateJob,
|
|
appendJobLog,
|
|
recordProductResult,
|
|
recordProductError,
|
|
finishJob,
|
|
cancelJob,
|
|
isJobCancelled,
|
|
getJob,
|
|
listJobs,
|
|
getLatestJobForShop,
|
|
};
|