← 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}`);
});