← back to Fashion Style Guides

server.js

112 lines

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

const PORT = process.env.PORT || 9826;
const ROOT = __dirname;
const DATA_DIR = path.join(ROOT, 'data');

const app = express();
app.use(express.json({ limit: '1mb' }));

// 404-guard — never serve snapshot/backup files even if they accidentally land in public/
app.use((req, res, next) => {
  if (/\.(bak)(\.|$)|\/\.?pre-|\.pre-/i.test(req.path)) {
    return res.status(404).type('text/plain').send('Not found');
  }
  next();
});

app.use(express.static(path.join(ROOT, 'public')));

const loadJson = (file) => JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8'));

app.get('/healthz', (_req, res) => res.json({ ok: true, ts: new Date().toISOString() }));

app.get('/api/brands', (_req, res) => {
  try { res.json(loadJson('brands.json')); }
  catch (err) { res.status(500).json({ ok: false, error: err.message }); }
});

app.get('/api/brands/:id', (req, res) => {
  try {
    const data = loadJson('brands.json');
    const brand = data.brands.find(b => b.id === req.params.id);
    if (!brand) return res.status(404).json({ ok: false, error: 'brand not found' });
    res.json(brand);
  } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
});

app.get('/api/pantone', (_req, res) => {
  try { res.json(loadJson('pantone.json')); }
  catch (err) { res.status(500).json({ ok: false, error: err.message }); }
});

app.get('/api/common-colors', (_req, res) => {
  try {
    const brands = loadJson('brands.json').brands;
    const buckets = {};
    const roleOrder = ['primary', 'secondary', 'accent', 'ink', 'ground', 'neutral'];

    brands.forEach(b => {
      b.palette.forEach(c => {
        if (!c.hex) return;
        const family = colorFamily(c.hex);
        if (!buckets[family]) buckets[family] = { family, count: 0, colors: [] };
        buckets[family].count += 1;
        buckets[family].colors.push({ hex: c.hex, name: c.name, brand: b.name, role: c.role });
      });
    });

    const sorted = Object.values(buckets).sort((a, b) => b.count - a.count);
    res.json({ ok: true, common_color_families: sorted, role_order: roleOrder });
  } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
});

app.get('/api/agent/status', (_req, res) => {
  const logFile = path.join(ROOT, 'agent', 'refresh.log');
  let lastRun = null;
  if (fs.existsSync(logFile)) {
    const stat = fs.statSync(logFile);
    const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
    lastRun = { mtime: stat.mtime, last_line: lines[lines.length - 1] || null, line_count: lines.length };
  }
  res.json({ ok: true, last_run: lastRun, log_path: logFile });
});

function colorFamily(hex) {
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const lightness = (max + min) / 2 / 255;
  const saturation = max === min ? 0 : (max - min) / (max + min);

  if (lightness > 0.92) return 'white';
  if (lightness < 0.08) return 'black';
  if (saturation < 0.12) return lightness > 0.55 ? 'cream' : 'gray';

  let hue;
  const d = (max - min) || 1;
  if (max === r) hue = ((g - b) / d) % 6;
  else if (max === g) hue = (b - r) / d + 2;
  else hue = (r - g) / d + 4;
  hue = (hue * 60 + 360) % 360;

  if (hue < 18 || hue >= 345) return 'red';
  if (hue < 45) return lightness < 0.45 ? 'brown' : 'tan';
  if (hue < 70) return 'gold';
  if (hue < 95) return 'yellow';
  if (hue < 165) return 'green';
  if (hue < 200) return 'teal';
  if (hue < 250) return 'blue';
  if (hue < 290) return 'purple';
  return 'pink';
}

app.listen(PORT, () => {
  console.log(`[fashion-style-guides] listening on http://127.0.0.1:${PORT}`);
});