← back to Watches
server.js
1404 lines
/**
* Omega Watch Price History - Enhanced Backend Server with SEO Optimization
* Port: 7600
* Enhanced with advanced APIs, caching, analytics, WebSocket support, and SEO features
*/
import express from 'express';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { spawn } from 'child_process';
import compression from 'compression';
import helmet from 'helmet';
import morgan from 'morgan';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import searchRouter from './api/search-simple.js';
import advancedSearchRouter from './api/advanced-search.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 7600;
const httpServer = createServer(app);
// Initialize WebSocket server
const wss = new WebSocketServer({ server: httpServer });
// In-memory cache
const cache = {
data: {},
timestamps: {},
TTL: 5 * 60 * 1000, // 5 minutes
get(key) {
if (this.data[key] && Date.now() - this.timestamps[key] < this.TTL) {
return this.data[key];
}
delete this.data[key];
delete this.timestamps[key];
return null;
},
set(key, value) {
this.data[key] = value;
this.timestamps[key] = Date.now();
},
clear() {
this.data = {};
this.timestamps = {};
},
stats() {
return {
keys: Object.keys(this.data).length,
size: JSON.stringify(this.data).length,
oldestEntry: Math.min(...Object.values(this.timestamps))
};
}
};
// View counter for trending watches
const viewCounts = {};
const watchlists = {}; // In-memory watchlists (would be DB in production)
// Middleware with SEO-friendly headers
// Disable helmet for now to avoid CSP issues
// app.use(helmet({
// contentSecurityPolicy: false,
// crossOriginEmbedderPolicy: false
// }));
app.use(compression());
app.use(morgan('combined'));
app.use(cors());
app.use(express.json());
// Basic Authentication Middleware
// Credentials must be supplied via DW_BASIC_AUTH in "user:password" form.
// Fail-closed at module load — never default to a hardcoded literal.
const DW_BASIC_AUTH = process.env.DW_BASIC_AUTH;
if (!DW_BASIC_AUTH) {
throw new Error('DW_BASIC_AUTH env var required for auth — set in .env before boot');
}
const _colonIdx = DW_BASIC_AUTH.indexOf(':');
if (_colonIdx <= 0 || _colonIdx === DW_BASIC_AUTH.length - 1) {
throw new Error('DW_BASIC_AUTH must be in "user:password" form');
}
const AUTH_USERNAME = DW_BASIC_AUTH.slice(0, _colonIdx);
const AUTH_PASSWORD = DW_BASIC_AUTH.slice(_colonIdx + 1);
app.use((req, res, next) => {
// Skip auth for health check endpoint
if (req.path === '/api/health' || req.path === '/health') {
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Omega Watch Price History"');
return res.status(401).send('Authentication required');
}
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
const [username, password] = credentials.split(':');
if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
return next();
}
res.setHeader('WWW-Authenticate', 'Basic realm="Omega Watch Price History"');
return res.status(401).send('Invalid credentials');
});
// Serve the search interface
app.get('/search', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'search-interface.html'));
});
// Snapshot-file 404 guard: never serve editor/git-merge leftovers from static roots
// Blocks .bak, .bak.*, .pre-*, .orig, .rej extensions anywhere in the path.
app.use((req, res, next) => {
if (/\.(bak|orig|rej)(\.|$)|\.pre-/i.test(req.path)) {
return res.status(404).type('text/plain').send('Not Found');
}
next();
});
// SEO-friendly static file serving with proper headers
app.use(express.static('dist', {
maxAge: '1d',
etag: true,
lastModified: true,
setHeaders: (res, filepath) => {
// Cache control for different file types
if (filepath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
} else if (filepath.match(/\.(js|css)$/)) {
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day
} else if (filepath.match(/\.(jpg|jpeg|png|gif|svg|ico|webp)$/)) {
res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days
}
}
}));
app.use(express.static('public', {
maxAge: '1d',
etag: true
}));
// Museum static files
app.use('/museum', express.static('museum', {
maxAge: '1h',
etag: true
}));
// Markdown docs (api-playground "Full Guide" link points at /docs/*.md)
app.use('/docs', express.static('docs', {
maxAge: '1h',
etag: true,
setHeaders: (res, filepath) => {
if (filepath.endsWith('.md')) res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
}
}));
// Request logging middleware with performance tracking
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
if (duration > 1000) {
console.log(`SLOW REQUEST: ${req.method} ${req.path} - ${duration}ms`);
}
});
next();
});
// Helper function to load watch data
function loadWatchData() {
const cached = cache.get('watches');
if (cached) return cached;
const data = JSON.parse(fs.readFileSync('./data/watches.json', 'utf8'));
cache.set('watches', data);
return data;
}
// Helper functions
function calculateAverageAppreciation(watches) {
const cached = cache.get('avgAppreciation');
if (cached !== null) return cached;
let totalAppreciation = 0;
let count = 0;
watches.forEach(watch => {
if (watch.priceHistory.length >= 2) {
const firstPrice = watch.priceHistory[0].price;
const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;
totalAppreciation += appreciation;
count++;
}
});
const result = count > 0 ? parseFloat((totalAppreciation / count).toFixed(2)) : 0;
cache.set('avgAppreciation', result);
return result;
}
function getTopPerformers(watches, limit) {
const cacheKey = `topPerformers_${limit}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const result = watches
.map(watch => {
if (watch.priceHistory.length < 2) return null;
const firstPrice = watch.priceHistory[0].price;
const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;
return {
id: watch.id,
model: watch.model,
series: watch.series,
appreciation: parseFloat(appreciation.toFixed(2)),
currentPrice: lastPrice,
yearIntroduced: watch.yearIntroduced
};
})
.filter(Boolean)
.sort((a, b) => parseFloat(b.appreciation) - parseFloat(a.appreciation))
.slice(0, limit);
cache.set(cacheKey, result);
return result;
}
function calculatePricePrediction(watch) {
if (watch.priceHistory.length < 3) {
return { error: 'Insufficient data for prediction' };
}
// Simple linear regression for trend prediction
const history = watch.priceHistory;
const n = history.length;
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
history.forEach((point, index) => {
const x = index;
const y = point.price;
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
});
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
// Predict next 3 years
const predictions = [];
const lastYear = history[history.length - 1].year;
for (let i = 1; i <= 3; i++) {
const year = lastYear + i;
const predictedPrice = Math.round(slope * (n + i - 1) + intercept);
predictions.push({
year,
predictedPrice: Math.max(0, predictedPrice),
confidence: calculateConfidence(history, slope)
});
}
return {
model: watch.model,
currentPrice: history[history.length - 1].price,
trend: slope > 0 ? 'increasing' : 'decreasing',
averageAnnualChange: Math.round((slope / (history[history.length - 1].price)) * 100 * 100) / 100,
predictions
};
}
function calculateConfidence(history, slope) {
// Calculate R-squared as confidence metric
const n = history.length;
let sumSquaredError = 0;
let sumSquaredTotal = 0;
const meanY = history.reduce((sum, p) => sum + p.price, 0) / n;
history.forEach((point, index) => {
const predicted = slope * index + (meanY - slope * (n - 1) / 2);
sumSquaredError += Math.pow(point.price - predicted, 2);
sumSquaredTotal += Math.pow(point.price - meanY, 2);
});
const rSquared = 1 - (sumSquaredError / sumSquaredTotal);
return Math.round(Math.max(0, Math.min(1, rSquared)) * 100);
}
// WebSocket connection handling
wss.on('connection', (ws) => {
console.log('New WebSocket client connected');
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'subscribe') {
ws.watchId = data.watchId;
ws.send(JSON.stringify({ type: 'subscribed', watchId: data.watchId }));
}
} catch (error) {
console.error('WebSocket message error:', error);
}
});
ws.on('close', () => {
console.log('WebSocket client disconnected');
});
});
// Broadcast function for WebSocket updates
function broadcastUpdate(watchId, data) {
wss.clients.forEach((client) => {
if (client.watchId === watchId && client.readyState === 1) {
client.send(JSON.stringify({ type: 'update', watchId, data }));
}
});
}
// ============================================================================
// SEO ROUTES
// ============================================================================
// Serve robots.txt
app.get('/robots.txt', (req, res) => {
res.type('text/plain');
res.sendFile(path.join(__dirname, 'public', 'robots.txt'));
});
// Serve sitemap.xml
app.get('/sitemap.xml', (req, res) => {
res.type('application/xml');
res.sendFile(path.join(__dirname, 'public', 'sitemap.xml'));
});
// Generate dynamic sitemap with all watches
app.get('/sitemap-dynamic.xml', (req, res) => {
const data = loadWatchData();
const baseUrl = 'http://45.61.58.125:7600';
let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
// Homepage
sitemap += ` <url>\n`;
sitemap += ` <loc>${baseUrl}/</loc>\n`;
sitemap += ` <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>\n`;
sitemap += ` <changefreq>daily</changefreq>\n`;
sitemap += ` <priority>1.0</priority>\n`;
sitemap += ` </url>\n`;
// Individual watches
data.watches.forEach(watch => {
sitemap += ` <url>\n`;
sitemap += ` <loc>${baseUrl}/?watch=${watch.id}</loc>\n`;
sitemap += ` <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>\n`;
sitemap += ` <changefreq>monthly</changefreq>\n`;
sitemap += ` <priority>0.8</priority>\n`;
sitemap += ` </url>\n`;
});
sitemap += '</urlset>';
res.type('application/xml');
res.send(sitemap);
});
// ============================================================================
// API ROUTES
// ============================================================================
// Mount comprehensive FTS5 search router
app.use('/api/search', searchRouter);
// Mount advanced search router with sorting and filtering
app.use('/api/advanced-search', advancedSearchRouter);
// Health check endpoint with detailed metrics
app.get('/api/health', (req, res) => {
const data = loadWatchData();
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
cache: cache.stats(),
database: {
watches: data.watches.length,
collections: [...new Set(data.watches.map(w => w.series))].length
},
websocket: {
connections: wss.clients.size
}
});
});
// Get all watches with optional filtering and pagination
app.get('/api/watches', (req, res) => {
const data = loadWatchData();
let watches = [...data.watches];
// Filter by series
if (req.query.series) {
watches = watches.filter(w => w.series === req.query.series);
}
// Filter by year range
if (req.query.minYear) {
watches = watches.filter(w => w.yearIntroduced >= parseInt(req.query.minYear));
}
if (req.query.maxYear) {
watches = watches.filter(w => w.yearIntroduced <= parseInt(req.query.maxYear));
}
// Filter by price range (current price)
if (req.query.minPrice || req.query.maxPrice) {
watches = watches.filter(w => {
const currentPrice = w.priceHistory[w.priceHistory.length - 1].price;
const min = req.query.minPrice ? parseInt(req.query.minPrice) : 0;
const max = req.query.maxPrice ? parseInt(req.query.maxPrice) : Infinity;
return currentPrice >= min && currentPrice <= max;
});
}
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 50;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const paginatedWatches = watches.slice(startIndex, endIndex);
res.json({
total: watches.length,
page,
limit,
totalPages: Math.ceil(watches.length / limit),
watches: paginatedWatches
});
});
// Advanced search endpoint
app.get('/api/search', (req, res) => {
const data = loadWatchData();
const query = req.query.q ? req.query.q.toLowerCase() : '';
let results = data.watches.filter(watch => {
const searchFields = [
watch.model,
watch.series,
watch.reference,
watch.description,
watch.specifications?.movement,
watch.specifications?.caseMaterial
].filter(Boolean).join(' ').toLowerCase();
return searchFields.includes(query);
});
// Apply filters
if (req.query.series) {
results = results.filter(w => w.series === req.query.series);
}
if (req.query.movement) {
results = results.filter(w =>
w.specifications?.movement?.toLowerCase().includes(req.query.movement.toLowerCase())
);
}
if (req.query.minPrice || req.query.maxPrice) {
results = results.filter(w => {
const currentPrice = w.priceHistory[w.priceHistory.length - 1].price;
const min = req.query.minPrice ? parseInt(req.query.minPrice) : 0;
const max = req.query.maxPrice ? parseInt(req.query.maxPrice) : Infinity;
return currentPrice >= min && currentPrice <= max;
});
}
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
res.json({
query,
total: results.length,
page,
limit,
totalPages: Math.ceil(results.length / limit),
results: results.slice(startIndex, endIndex)
});
});
// Get trending watches (most viewed)
app.get('/api/watches/trending', (req, res) => {
const limit = parseInt(req.query.limit) || 10;
const trending = Object.entries(viewCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([watchId, views]) => {
const data = loadWatchData();
const watch = data.watches.find(w => w.id === watchId);
return watch ? { ...watch, views } : null;
})
.filter(Boolean);
res.json({
trending,
totalViews: Object.values(viewCounts).reduce((sum, v) => sum + v, 0)
});
});
// Get price history for specific watch
app.get('/api/watches/:id/history', (req, res) => {
const data = loadWatchData();
const watch = data.watches.find(w => w.id === req.params.id);
if (!watch) {
return res.status(404).json({ error: 'Watch not found' });
}
// Track view count
viewCounts[req.params.id] = (viewCounts[req.params.id] || 0) + 1;
res.json({
watch: {
id: watch.id,
model: watch.model,
series: watch.series,
reference: watch.reference,
yearIntroduced: watch.yearIntroduced,
specifications: watch.specifications
},
priceHistory: watch.priceHistory,
views: viewCounts[req.params.id]
});
});
// Get price predictions for a watch
app.get('/api/price-predictions/:id', (req, res) => {
const data = loadWatchData();
const watch = data.watches.find(w => w.id === req.params.id);
if (!watch) {
return res.status(404).json({ error: 'Watch not found' });
}
const prediction = calculatePricePrediction(watch);
res.json(prediction);
});
// Get collections with statistics
app.get('/api/collections', (req, res) => {
const cacheKey = 'collections_stats';
const cached = cache.get(cacheKey);
if (cached) return res.json(cached);
const data = loadWatchData();
const collections = {};
data.watches.forEach(watch => {
if (!collections[watch.series]) {
collections[watch.series] = {
series: watch.series,
count: 0,
watches: [],
avgPrice: 0,
totalAppreciation: 0,
avgYearIntroduced: 0
};
}
const coll = collections[watch.series];
coll.count++;
coll.watches.push({
id: watch.id,
model: watch.model,
reference: watch.reference,
yearIntroduced: watch.yearIntroduced
});
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
coll.avgPrice += currentPrice;
coll.avgYearIntroduced += watch.yearIntroduced;
if (watch.priceHistory.length >= 2) {
const firstPrice = watch.priceHistory[0].price;
const appreciation = ((currentPrice - firstPrice) / firstPrice) * 100;
coll.totalAppreciation += appreciation;
}
});
// Calculate averages
Object.values(collections).forEach(coll => {
coll.avgPrice = Math.round(coll.avgPrice / coll.count);
coll.avgAppreciation = Math.round((coll.totalAppreciation / coll.count) * 100) / 100;
coll.avgYearIntroduced = Math.round(coll.avgYearIntroduced / coll.count);
delete coll.totalAppreciation;
});
const result = {
collections: Object.values(collections).sort((a, b) => b.count - a.count),
totalCollections: Object.keys(collections).length
};
cache.set(cacheKey, result);
res.json(result);
});
// Get statistics
app.get('/api/statistics', (req, res) => {
const data = loadWatchData();
const stats = {
totalWatches: data.watches.length,
totalSeries: [...new Set(data.watches.map(w => w.series))].length,
averageAppreciation: calculateAverageAppreciation(data.watches),
topPerformers: getTopPerformers(data.watches, 5),
lastUpdated: new Date().toISOString(),
priceRanges: calculatePriceRanges(data.watches),
movementTypes: calculateMovementDistribution(data.watches),
appreciationByDecade: calculateAppreciationByDecade(data.watches)
};
res.json(stats);
});
// Analytics: Price ranges distribution
function calculatePriceRanges(watches) {
const cacheKey = 'priceRanges';
const cached = cache.get(cacheKey);
if (cached) return cached;
const ranges = {
'Under $5,000': 0,
'$5,000 - $10,000': 0,
'$10,000 - $25,000': 0,
'$25,000 - $50,000': 0,
'Over $50,000': 0
};
watches.forEach(watch => {
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
if (currentPrice < 5000) ranges['Under $5,000']++;
else if (currentPrice < 10000) ranges['$5,000 - $10,000']++;
else if (currentPrice < 25000) ranges['$10,000 - $25,000']++;
else if (currentPrice < 50000) ranges['$25,000 - $50,000']++;
else ranges['Over $50,000']++;
});
cache.set(cacheKey, ranges);
return ranges;
}
// Analytics: Movement type distribution
function calculateMovementDistribution(watches) {
const cacheKey = 'movementTypes';
const cached = cache.get(cacheKey);
if (cached) return cached;
const movements = {};
watches.forEach(watch => {
if (watch.specifications?.movement) {
const movementType = watch.specifications.movement.includes('automatic') ? 'Automatic' :
watch.specifications.movement.includes('manual') ? 'Manual' :
'Other';
movements[movementType] = (movements[movementType] || 0) + 1;
}
});
cache.set(cacheKey, movements);
return movements;
}
// Analytics: Appreciation by decade
function calculateAppreciationByDecade(watches) {
const cacheKey = 'appreciationByDecade';
const cached = cache.get(cacheKey);
if (cached) return cached;
const decades = {};
watches.forEach(watch => {
const decade = Math.floor(watch.yearIntroduced / 10) * 10;
if (!decades[decade]) {
decades[decade] = { count: 0, totalAppreciation: 0 };
}
if (watch.priceHistory.length >= 2) {
const firstPrice = watch.priceHistory[0].price;
const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;
decades[decade].count++;
decades[decade].totalAppreciation += appreciation;
}
});
const result = {};
Object.keys(decades).forEach(decade => {
result[`${decade}s`] = Math.round(
(decades[decade].totalAppreciation / decades[decade].count) * 100
) / 100;
});
cache.set(cacheKey, result);
return result;
}
// Compare multiple watches
app.post('/api/compare', (req, res) => {
const { watchIds } = req.body;
if (!watchIds || !Array.isArray(watchIds)) {
return res.status(400).json({ error: 'watchIds array is required' });
}
const data = loadWatchData();
const comparison = watchIds.map(id => {
const watch = data.watches.find(w => w.id === id);
if (!watch) return null;
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
const firstPrice = watch.priceHistory[0].price;
const appreciation = ((currentPrice - firstPrice) / firstPrice) * 100;
return {
id: watch.id,
model: watch.model,
series: watch.series,
yearIntroduced: watch.yearIntroduced,
priceHistory: watch.priceHistory,
specifications: watch.specifications,
currentPrice,
appreciation: Math.round(appreciation * 100) / 100
};
}).filter(Boolean);
res.json({
watches: comparison,
count: comparison.length
});
});
// Export data in different formats
app.get('/api/export/:format', (req, res) => {
const data = loadWatchData();
const format = req.params.format.toLowerCase();
if (format === 'json') {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', 'attachment; filename=omega-watches.json');
res.json(data);
} else if (format === 'csv') {
const csv = convertToCSV(data.watches);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=omega-watches.csv');
res.send(csv);
} else {
res.status(400).json({ error: 'Unsupported format. Use json or csv' });
}
});
function convertToCSV(watches) {
const headers = ['ID', 'Model', 'Series', 'Reference', 'Year Introduced', 'Current Price', 'Original Price', 'Appreciation %'];
const rows = [headers.join(',')];
watches.forEach(watch => {
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
const originalPrice = watch.priceHistory[0].price;
const appreciation = ((currentPrice - originalPrice) / originalPrice) * 100;
rows.push([
watch.id,
`"${watch.model}"`,
watch.series,
watch.reference,
watch.yearIntroduced,
currentPrice,
originalPrice,
Math.round(appreciation * 100) / 100
].join(','));
});
return rows.join('\n');
}
// Load OpenAPI specification
const swaggerDocument = YAML.load(path.join(__dirname, 'openapi.yaml'));
// Market Data Cache (1 hour TTL)
const marketDataCache = {
data: {},
timestamps: {},
TTL: 60 * 60 * 1000, // 1 hour
get(key) {
if (this.data[key] && Date.now() - this.timestamps[key] < this.TTL) {
return this.data[key];
}
delete this.data[key];
delete this.timestamps[key];
return null;
},
set(key, value) {
this.data[key] = value;
this.timestamps[key] = Date.now();
}
};
// Load watch-wayback mapping
let watchWaybackMap = {};
try {
const mapPath = path.join(__dirname, 'data', 'watch-wayback-map.json');
if (fs.existsSync(mapPath)) {
watchWaybackMap = JSON.parse(fs.readFileSync(mapPath, 'utf8')).watches || {};
}
} catch (e) {
console.warn('Warning: Could not load watch-wayback-map.json:', e.message);
}
// Mock market data fallback
function getMockMarketData(watchId) {
const mockData = {
'speedmaster-moonwatch': { low: 6500, high: 8500, avg: 7200 },
'speedmaster-alaska': { low: 18000, high: 28000, avg: 22000 },
'seamaster-300': { low: 14000, high: 20000, avg: 16500 },
'seamaster-ploprof': { low: 10000, high: 16000, avg: 13000 },
'seamaster-bond': { low: 4800, high: 7500, avg: 5800 },
'constellation-piepan': { low: 2500, high: 6000, avg: 3800 },
'constellation-manhattan': { low: 4000, high: 7000, avg: 5200 },
'deville-tresor': { low: 3500, high: 5500, avg: 4300 },
'deville-coaxial': { low: 3000, high: 5000, avg: 3800 },
'railmaster-original': { low: 4500, high: 8000, avg: 6000 },
'aqua-terra': { low: 3500, high: 6000, avg: 4500 },
'planet-ocean': { low: 4500, high: 7500, avg: 5800 },
'speedmaster-reduced': { low: 2800, high: 4500, avg: 3500 },
'speedmaster-mark2': { low: 3500, high: 6000, avg: 4500 },
'flightmaster': { low: 5000, high: 10000, avg: 7000 }
};
const data = mockData[watchId] || { low: 3000, high: 8000, avg: 5000 };
return {
success: true,
model: watchId,
marketPrice: {
low: data.low,
high: data.high,
avg: data.avg,
currency: 'USD'
},
source: 'Mock Data',
isMock: true,
timestamp: Date.now(),
fetchedAt: new Date().toISOString()
};
}
// Call Python Wayback scraper
function callWaybackScraper(model) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, 'scripts', 'wayback-price-scraper.py');
const python = spawn('python3', [
scriptPath,
'--model', model,
'--limit', '20',
'--delay', '0.5'
], {
timeout: 120000 // 2 minute timeout
});
let stdout = '';
let stderr = '';
python.stdout.on('data', (data) => {
stdout += data.toString();
});
python.stderr.on('data', (data) => {
stderr += data.toString();
});
python.on('close', (code) => {
if (code === 0 && stdout) {
try {
const result = JSON.parse(stdout);
resolve(result);
} catch (e) {
reject(new Error(`Failed to parse scraper output: ${e.message}`));
}
} else {
reject(new Error(stderr || `Scraper exited with code ${code}`));
}
});
python.on('error', (err) => {
reject(err);
});
});
}
// Market Data API Endpoint
app.get('/api/market-data/:watchId', async (req, res) => {
const { watchId } = req.params;
// Check cache first
const cached = marketDataCache.get(watchId);
if (cached) {
return res.json({
...cached,
cached: true,
cacheAge: Date.now() - marketDataCache.timestamps[watchId]
});
}
// Get wayback model from mapping
const mapping = watchWaybackMap[watchId];
const waybackModel = mapping?.waybackModel || 'speedmaster';
try {
console.log(`Fetching market data for ${watchId} (wayback model: ${waybackModel})`);
const result = await callWaybackScraper(waybackModel);
if (result.success && result.statistics?.avgPrice) {
const marketData = {
success: true,
model: watchId,
marketPrice: {
low: result.statistics.minPrice,
high: result.statistics.maxPrice,
avg: result.statistics.avgPrice,
currency: 'USD'
},
priceHistory: result.priceHistory || [],
pagesFound: result.pagesFound,
pagesScraped: result.pagesScraped,
source: 'Wayback Machine (Chrono24 Archives)',
isMock: false,
timestamp: Date.now(),
fetchedAt: new Date().toISOString()
};
// Cache the result
marketDataCache.set(watchId, marketData);
return res.json(marketData);
} else {
// Fallback to mock data
console.log(`No real data found for ${watchId}, using mock data`);
const mockData = getMockMarketData(watchId);
marketDataCache.set(watchId, mockData);
return res.json(mockData);
}
} catch (error) {
console.error(`Market data fetch error for ${watchId}:`, error.message);
const mockData = getMockMarketData(watchId);
return res.json(mockData);
}
});
// Historical Prices API Endpoint
app.get('/api/historical-prices/:watchId', (req, res) => {
const { watchId } = req.params;
const { from, to, granularity = 'monthly' } = req.query;
try {
// Load aggregated data if available
const aggregatedPath = path.join(__dirname, 'data', 'aggregated-prices.json');
const rawPath = path.join(__dirname, 'data', 'historical-prices.json');
let responseData = null;
// Try aggregated data first
if (fs.existsSync(aggregatedPath)) {
const aggregated = JSON.parse(fs.readFileSync(aggregatedPath, 'utf8'));
if (aggregated.watches && aggregated.watches[watchId]) {
const watchData = aggregated.watches[watchId];
// Filter by date range if specified
let data = granularity === 'yearly' ? watchData.yearly : watchData.monthly;
if (from) {
data = data.filter(d => (d.month || d.year.toString()) >= from);
}
if (to) {
data = data.filter(d => (d.month || d.year.toString()) <= to);
}
responseData = {
success: true,
watchId,
granularity,
dateRange: watchData.dateRange,
statistics: watchData.statistics,
trends: watchData.trends,
data,
source: 'aggregated'
};
}
}
// Fallback to raw data
if (!responseData && fs.existsSync(rawPath)) {
const raw = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
if (raw.prices && raw.prices[watchId]) {
let records = raw.prices[watchId];
// Filter by date range
if (from) {
records = records.filter(r => r.date >= from);
}
if (to) {
records = records.filter(r => r.date <= to);
}
responseData = {
success: true,
watchId,
granularity: 'daily',
records: records.length,
data: records.slice(0, 500), // Limit response size
source: 'raw'
};
}
}
if (responseData) {
return res.json(responseData);
}
// No data found
return res.json({
success: false,
error: 'No historical data available for this watch',
watchId,
message: 'Run the historical price crawler to populate data'
});
} catch (error) {
console.error('Historical prices error:', error);
return res.status(500).json({
success: false,
error: error.message
});
}
});
// Get all historical data summary
app.get('/api/historical-prices', (req, res) => {
try {
const aggregatedPath = path.join(__dirname, 'data', 'aggregated-prices.json');
const rawPath = path.join(__dirname, 'data', 'historical-prices.json');
let summary = {
hasData: false,
watches: [],
totalRecords: 0,
lastUpdated: null
};
if (fs.existsSync(aggregatedPath)) {
const aggregated = JSON.parse(fs.readFileSync(aggregatedPath, 'utf8'));
summary.hasData = true;
summary.lastUpdated = aggregated.metadata?.processedAt;
summary.watches = Object.keys(aggregated.watches || {}).map(id => ({
id,
records: aggregated.watches[id].rawRecords,
dateRange: aggregated.watches[id].dateRange,
currentPrice: aggregated.watches[id].statistics?.avg
}));
summary.totalRecords = aggregated.metadata?.processedRecords || 0;
} else if (fs.existsSync(rawPath)) {
const raw = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
summary.hasData = raw.metadata?.totalRecords > 0;
summary.lastUpdated = raw.metadata?.lastUpdated;
summary.watches = Object.keys(raw.prices || {}).map(id => ({
id,
records: Array.isArray(raw.prices[id]) ? raw.prices[id].length : 0
}));
summary.totalRecords = raw.metadata?.totalRecords || 0;
}
return res.json(summary);
} catch (error) {
console.error('Historical prices summary error:', error);
return res.status(500).json({ error: error.message });
}
});
// ============================================
// ROLEX OFFICIAL PRICES API
// ============================================
// GET /api/rolex-prices - Get all Rolex current prices
app.get('/api/rolex-prices', (req, res) => {
try {
const dataFile = path.join(__dirname, 'data', 'rolex-official-prices.json');
if (!fs.existsSync(dataFile)) {
return res.status(404).json({ error: 'Rolex price data not found' });
}
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const latestScrape = data.scrapes[data.scrapes.length - 1];
// Filter by collection if specified
const { collection, minPrice, maxPrice, material } = req.query;
let products = latestScrape.products;
if (collection) {
products = products.filter(p => p.collection.toLowerCase().includes(collection.toLowerCase()));
}
if (minPrice) {
products = products.filter(p => p.price >= parseInt(minPrice));
}
if (maxPrice) {
products = products.filter(p => p.price <= parseInt(maxPrice));
}
if (material) {
products = products.filter(p => p.material && p.material.toLowerCase().includes(material.toLowerCase()));
}
res.json({
success: true,
date: latestScrape.date,
totalProducts: products.length,
products: products
});
} catch (error) {
res.status(500).json({ error: 'Failed to load Rolex prices', details: error.message });
}
});
// GET /api/rolex-prices/:reference - Get specific Rolex by reference
app.get('/api/rolex-prices/:reference', (req, res) => {
try {
const dataFile = path.join(__dirname, 'data', 'rolex-official-prices.json');
if (!fs.existsSync(dataFile)) {
return res.status(404).json({ error: 'Rolex price data not found' });
}
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const { reference } = req.params;
// Search across all scrapes for price history
const priceHistory = [];
for (const scrape of data.scrapes) {
const product = scrape.products.find(p =>
p.reference && p.reference.toLowerCase().includes(reference.toLowerCase())
);
if (product) {
priceHistory.push({
date: scrape.date,
price: product.price,
...product
});
}
}
if (priceHistory.length === 0) {
return res.status(404).json({ error: 'Watch not found', reference });
}
res.json({
success: true,
reference,
currentPrice: priceHistory[priceHistory.length - 1].price,
priceHistory
});
} catch (error) {
res.status(500).json({ error: 'Failed to load Rolex prices', details: error.message });
}
});
// GET /api/rolex-prices/history - Get historical Rolex data from Wayback crawl
app.get('/api/rolex-historical', (req, res) => {
try {
const dataFile = path.join(__dirname, 'data', 'rolex-historical-prices.json');
if (!fs.existsSync(dataFile)) {
return res.status(404).json({ error: 'Rolex historical data not found. Run the wayback crawler first.' });
}
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const { model, from, to } = req.query;
if (model && data.models[model]) {
const modelData = data.models[model];
let history = modelData.priceHistory;
// Filter by date range if specified
if (from || to) {
history = Object.fromEntries(
Object.entries(history).filter(([date]) => {
if (from && date < from) return false;
if (to && date > to) return false;
return true;
})
);
}
res.json({
success: true,
model: modelData.name,
totalDataPoints: Object.keys(history).length,
priceHistory: history
});
} else {
// Return summary of all models
const summary = Object.entries(data.models).map(([id, model]) => ({
id,
name: model.name,
dataPoints: model.totalDataPoints || 0
}));
res.json({
success: true,
metadata: data.metadata,
models: summary
});
}
} catch (error) {
res.status(500).json({ error: 'Failed to load Rolex historical data', details: error.message });
}
});
// ============================================
// OMEGA OFFICIAL PRICES API
// ============================================
// GET /api/omega-prices - Get all Omega current prices
app.get('/api/omega-prices', (req, res) => {
try {
const dataFile = path.join(__dirname, 'data', 'omega-official-prices.json');
if (!fs.existsSync(dataFile)) {
return res.status(404).json({ error: 'Omega price data not found' });
}
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const latestScrape = data.scrapes[data.scrapes.length - 1];
// Filter by collection if specified
const { collection, minPrice, maxPrice } = req.query;
let products = latestScrape.products;
if (collection) {
products = products.filter(p => p.collection.toLowerCase().includes(collection.toLowerCase()));
}
if (minPrice) {
products = products.filter(p => p.price >= parseInt(minPrice));
}
if (maxPrice) {
products = products.filter(p => p.price <= parseInt(maxPrice));
}
res.json({
success: true,
date: latestScrape.date,
totalProducts: products.length,
products: products
});
} catch (error) {
res.status(500).json({ error: 'Failed to load Omega prices', details: error.message });
}
});
// GET /api/omega-prices/:reference - Get specific Omega by reference
app.get('/api/omega-prices/:reference', (req, res) => {
try {
const dataFile = path.join(__dirname, 'data', 'omega-official-prices.json');
if (!fs.existsSync(dataFile)) {
return res.status(404).json({ error: 'Omega price data not found' });
}
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const { reference } = req.params;
// Search across all scrapes for price history
const priceHistory = [];
for (const scrape of data.scrapes) {
const product = scrape.products.find(p =>
p.reference && p.reference.toLowerCase().includes(reference.toLowerCase())
);
if (product) {
priceHistory.push({
date: scrape.date,
price: product.price,
...product
});
}
}
if (priceHistory.length === 0) {
return res.status(404).json({ error: 'Watch not found', reference });
}
res.json({
success: true,
reference,
currentPrice: priceHistory[priceHistory.length - 1].price,
priceHistory
});
} catch (error) {
res.status(500).json({ error: 'Failed to load Omega prices', details: error.message });
}
});
// Swagger UI options
const swaggerOptions = {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'Omega Watch Price History API',
customfavIcon: '/favicon.ico'
};
// Serve Swagger UI at /api/docs/swagger
app.use('/api/docs/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));
// OpenAPI JSON endpoint
app.get('/api/docs/openapi.json', (req, res) => {
res.json(swaggerDocument);
});
// OpenAPI YAML endpoint
app.get('/api/docs/openapi.yaml', (req, res) => {
res.setHeader('Content-Type', 'text/yaml');
res.send(fs.readFileSync(path.join(__dirname, 'openapi.yaml'), 'utf8'));
});
// API Documentation endpoint (legacy - redirect to Swagger)
app.get('/api/docs', (req, res) => {
res.redirect('/api/docs/swagger');
});
// 404 guard: unknown /api/* paths must return JSON 404, not the SPA shell
app.use('/api', (req, res) => {
res.status(404).json({ error: 'Not Found', path: req.originalUrl });
});
// Fallback route - serve React app with SEO-friendly headers
app.get('*', (req, res) => {
res.set({
'Cache-Control': 'no-cache, must-revalidate',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block'
});
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// Start server
httpServer.listen(PORT, '0.0.0.0', () => {
console.log(`
╔════════════════════════════════════════════════════════════════╗
║ OMEGA WATCH PRICE HISTORY - SEO OPTIMIZED ║
╚════════════════════════════════════════════════════════════════╝
🚀 Server running on port ${PORT}
📊 Dashboard: http://45.61.58.125:${PORT}
🔌 API: http://45.61.58.125:${PORT}/api
📖 API Docs: http://45.61.58.125:${PORT}/api/docs
🌐 WebSocket: ws://45.61.58.125:${PORT}
🗺️ Sitemap: http://45.61.58.125:${PORT}/sitemap.xml
🤖 Robots: http://45.61.58.125:${PORT}/robots.txt
SEO FEATURES:
✓ Dynamic meta tags & structured data
✓ Sitemap.xml with 32 watch pages
✓ Robots.txt for crawler control
✓ Open Graph & Twitter Cards
✓ Performance optimization headers
✓ Semantic HTML5 structure
✓ Mobile-first responsive design
✓ Core Web Vitals optimized
PRODUCTION READY!
`);
});