Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
lejay
2026-10-07 17:13:03 +02:00
co-authored by Cursor
parent 6c0d7e47b8
commit 30921aade6
75 changed files with 15618 additions and 1300 deletions
+172
View File
@@ -1,5 +1,6 @@
import { sync, getSyncLogs, getMaterials } from '../services/formexCrawler.js';
import { syncHaefeleByUrls } from '../services/haefeleCrawler.js';
import { syncLeubaHiag, LEUBA_HIAG_TARGETS } from '../services/leubaHiagCrawler.js';
import { getSchedulerStatus } from '../services/scheduler.js';
import pool, { run } from '../db/index.js';
@@ -206,6 +207,150 @@ export async function getHaefeleMaterials(req, res) {
}
}
/**
* GET /api/sync/leubahiag/targets
* List the curated Leuba Hiag catalog lines available for import.
*/
export function getLeubaHiagTargets(req, res) {
res.json(LEUBA_HIAG_TARGETS.map(({ key, label, tier }) => ({ key, label, tier })));
}
function parseTargetsParam(value) {
if (Array.isArray(value)) return value.filter(Boolean);
if (typeof value === 'string' && value.trim()) {
return value.split(/[\n,;]+/g).map((v) => v.trim()).filter(Boolean);
}
return null;
}
/**
* POST /api/sync/leubahiag
* Trigger a Leuba Hiag curated catalog sync (non-streaming).
* Body: { targets?: string[], activate?: boolean, credentials?: {...} }
*/
export async function triggerLeubaHiagSync(req, res) {
try {
const targets = parseTargetsParam(req.body?.targets);
const activate = req.body?.activate === true || req.body?.activate === '1';
const fetchPrices = req.body?.fetchPrices !== false && req.body?.fetchPrices !== '0';
const credentials = {
username: req.body?.username || req.body?.email,
password: req.body?.password,
};
const result = await syncLeubaHiag({ targets, activate, fetchPrices, credentials }, () => {});
if (!result.success) {
return res.status(500).json({
success: false,
error: result.error || 'Sync Leuba Hiag échouée',
itemsCount: result.itemsCount || 0,
});
}
res.json({
success: true,
message: `Synchronisation Leuba Hiag terminée: ${result.itemsCount} produits`,
itemsCount: result.itemsCount,
createdCount: result.createdCount || 0,
updatedCount: result.updatedCount || 0,
pricedCount: result.pricedCount || 0,
duration: result.duration,
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
}
/**
* GET /api/sync/leubahiag/stream
* SSE endpoint for Leuba Hiag sync progress.
* Query: targets=key1,key2 (optional), activate=1 (optional)
*/
export async function streamLeubaHiagSync(req, res) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const emit = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
req.on('close', () => {
// client disconnected
});
const targets = parseTargetsParam(req.query.targets);
const activate = req.query.activate === '1' || req.query.activate === 'true';
const fetchPrices = req.query.prices !== '0' && req.query.prices !== 'false';
try {
await syncLeubaHiag({ targets, activate, fetchPrices }, emit);
} catch (err) {
emit({ type: 'error', error: err.message });
}
res.end();
}
/**
* GET /api/leubahiag/materials
* List Leuba Hiag materials that were imported.
*/
export async function getLeubaHiagMaterials(req, res) {
try {
const { search = '', active, page = '1', limit = '25' } = req.query;
const pageNum = Math.max(1, parseInt(page, 10));
const limitNum = Math.min(200, Math.max(1, parseInt(limit, 10)));
const offset = (pageNum - 1) * limitNum;
const whereClauses = [
`(supplier_id = (SELECT id FROM suppliers WHERE code = 'leubahiag') OR formex_code LIKE 'LH-%')`
];
const params = [];
if (search) {
whereClauses.push('(name LIKE ? OR formex_code LIKE ? OR collection LIKE ?)');
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
}
if (active === '1' || active === 'true') {
whereClauses.push('active = 1');
} else if (active === '0' || active === 'false') {
whereClauses.push('active = 0');
}
const whereSQL = `WHERE ${whereClauses.join(' AND ')}`;
const countResult = await pool.query(
`SELECT COUNT(*) as total FROM formex_materials ${whereSQL}`,
params
);
const total = countResult.rows[0]?.total || 0;
const { rows } = await pool.query(
`SELECT id, formex_code, name, collection, category, thickness_mm, width_mm, height_mm,
price_chf, price_unit, image_url, formex_url, active, last_synced_at
FROM formex_materials
${whereSQL}
ORDER BY collection, name
LIMIT ? OFFSET ?`,
[...params, limitNum, offset]
);
res.json({
materials: rows,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
}
/**
* GET /api/sync/logs
* Get sync history
@@ -439,6 +584,33 @@ export async function updateFormexMaterial(req, res) {
}
}
/**
* PATCH /api/formex/materials/:id/active
* Toggle only the `active` flag of a material (lossless: never touches other columns).
* Body: { active: boolean }
*/
export async function setFormexMaterialActive(req, res) {
try {
const { id } = req.params;
const active = req.body?.active === true || req.body?.active === 1 || req.body?.active === '1';
const existing = await pool.query('SELECT id FROM formex_materials WHERE id = ?', [id]);
if (existing.rows.length === 0) {
return res.status(404).json({ error: 'Matériau non trouvé' });
}
await run('UPDATE formex_materials SET active = ? WHERE id = ?', [active ? 1 : 0, id]);
const { rows } = await pool.query(
'SELECT id, formex_code, name, active FROM formex_materials WHERE id = ?',
[id]
);
res.json(rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
/**
* DELETE /api/formex/materials/:id
* Delete a material