← back to Astek Import Viewer

server.js

132 lines

'use strict';

const express = require('express');
const fs = require('fs');
const path = require('path');
const net = require('net');

const DATA_DIR = path.join(__dirname, 'data');
const DATA_FILE = path.join(DATA_DIR, 'astek.json');
const SELECTIONS_FILE = path.join(DATA_DIR, 'selections.json');
const PUBLIC_DIR = path.join(__dirname, 'public');

const PREFERRED_PORT = 9789;
const PORT_RANGE_END = 9799; // scan 9789..9799 for a free 97xx port

// ---- Load catalog once into memory ----
let PRODUCTS = [];
try {
  PRODUCTS = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
  if (!Array.isArray(PRODUCTS)) throw new Error('astek.json is not an array');
  console.log(`Loaded ${PRODUCTS.length} products from ${DATA_FILE}`);
} catch (err) {
  console.error(`FATAL: could not load ${DATA_FILE}:`, err.message);
  process.exit(1);
}

// ---- Selections persistence helpers ----
function readSelections() {
  try {
    const raw = fs.readFileSync(SELECTIONS_FILE, 'utf8');
    const arr = JSON.parse(raw);
    return Array.isArray(arr) ? arr : [];
  } catch (_) {
    return [];
  }
}

function writeSelections(ids) {
  const clean = Array.from(new Set((Array.isArray(ids) ? ids : []))).filter(
    (v) => v !== null && v !== undefined
  );
  fs.writeFileSync(SELECTIONS_FILE, JSON.stringify(clean, null, 2));
  return clean;
}

function csvCell(v) {
  if (v === null || v === undefined) return '';
  const s = String(v);
  if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  return s;
}

// ---- App ----
const app = express();
app.use(express.json({ limit: '5mb' }));
app.use(express.static(PUBLIC_DIR));

// Full catalog
app.get('/api/products', (req, res) => {
  res.json(PRODUCTS);
});

// Persist the selected id list
app.post('/api/select', (req, res) => {
  const ids = req.body && req.body.ids;
  if (!Array.isArray(ids)) {
    return res.status(400).json({ error: 'body must be {ids:[...]}' });
  }
  const saved = writeSelections(ids);
  res.json({ ok: true, count: saved.length });
});

// Read selections back
app.get('/api/selections', (req, res) => {
  res.json({ ids: readSelections() });
});

// Export selected rows as CSV download
app.get('/api/export', (req, res) => {
  const selected = new Set(readSelections().map((v) => String(v)));
  const cols = ['dw_sku', 'mfr_sku', 'title', 'pattern_name', 'color_name', 'price'];
  const rows = PRODUCTS.filter((p) => selected.has(String(p.id)));
  const lines = [cols.join(',')];
  for (const r of rows) {
    lines.push(cols.map((c) => csvCell(r[c])).join(','));
  }
  const csv = lines.join('\n');
  const stamp = new Date().toISOString().slice(0, 10);
  res.setHeader('Content-Type', 'text/csv; charset=utf-8');
  res.setHeader(
    'Content-Disposition',
    `attachment; filename="astek-selection-${stamp}.csv"`
  );
  res.send(csv);
});

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', products: PRODUCTS.length, selected: readSelections().length });
});

// ---- Port scan then listen ----
function checkPort(port) {
  return new Promise((resolve) => {
    const tester = net
      .createServer()
      .once('error', () => resolve(false))
      .once('listening', () => tester.close(() => resolve(true)))
      .listen(port, '127.0.0.1');
  });
}

async function findPort() {
  for (let p = PREFERRED_PORT; p <= PORT_RANGE_END; p++) {
    if (await checkPort(p)) return p;
  }
  throw new Error(`No free port in ${PREFERRED_PORT}..${PORT_RANGE_END}`);
}

findPort()
  .then((port) => {
    app.listen(port, () => {
      if (port !== PREFERRED_PORT) {
        console.log(`NOTE: preferred port ${PREFERRED_PORT} taken; using ${port}`);
      }
      console.log(`astek-import-viewer listening on http://localhost:${port}`);
    });
  })
  .catch((err) => {
    console.error('FATAL:', err.message);
    process.exit(1);
  });