feat: track prices by source
This commit is contained in:
74
server.js
74
server.js
@@ -63,6 +63,10 @@ function normalizeKey(value) {
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function normalizeSourceLabel(value) {
|
||||
return String(value || 'Unknown source').trim() || 'Unknown source';
|
||||
}
|
||||
|
||||
const HISTORY_KEY_ALIASES = {
|
||||
'costco-breathless': 'hotel-breathless',
|
||||
'costco-grand-fiesta': 'hotel-grand-fiesta',
|
||||
@@ -194,7 +198,8 @@ function loadPriceHistoryState() {
|
||||
price,
|
||||
currency: point.currency || 'USD',
|
||||
displayPrice: point.displayPrice || point.priceLabel || point.label || null,
|
||||
source: point.source || null,
|
||||
source: normalizeSourceLabel(point.source || point.sourceLabel || point.vendor || null),
|
||||
sourceKey: normalizeKey(point.sourceKey || point.sourceId || point.source || point.sourceLabel || point.vendor || 'unknown-source'),
|
||||
sourceUrl: point.sourceUrl || point.url || null,
|
||||
note: point.note || point.description || null,
|
||||
availability: point.availability || point.status || null,
|
||||
@@ -223,6 +228,61 @@ function loadPriceHistoryState() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPriceHistoryBySource(priceHistory) {
|
||||
const grouped = new Map();
|
||||
|
||||
priceHistory.forEach((point) => {
|
||||
const sourceKey = normalizeKey(point.sourceKey || point.source || 'unknown-source');
|
||||
const sourceLabel = normalizeSourceLabel(point.source || point.sourceLabel || sourceKey);
|
||||
if (!grouped.has(sourceKey)) {
|
||||
grouped.set(sourceKey, {
|
||||
sourceKey,
|
||||
sourceLabel,
|
||||
sourceUrl: point.sourceUrl || null,
|
||||
points: [],
|
||||
});
|
||||
}
|
||||
|
||||
const bucket = grouped.get(sourceKey);
|
||||
bucket.points.push({
|
||||
...point,
|
||||
sourceKey,
|
||||
source: sourceLabel,
|
||||
});
|
||||
if (!bucket.sourceUrl && point.sourceUrl) bucket.sourceUrl = point.sourceUrl;
|
||||
if (bucket.sourceLabel === 'Unknown source' && sourceLabel) bucket.sourceLabel = sourceLabel;
|
||||
});
|
||||
|
||||
const seriesBySource = {};
|
||||
const sourceSummaries = [...grouped.values()].map((bucket) => {
|
||||
bucket.points.sort((a, b) => a.runIndex - b.runIndex);
|
||||
seriesBySource[bucket.sourceKey] = bucket.points;
|
||||
const latestPoint = bucket.points.at(-1) || null;
|
||||
|
||||
return {
|
||||
sourceKey: bucket.sourceKey,
|
||||
sourceLabel: bucket.sourceLabel,
|
||||
sourceUrl: bucket.sourceUrl,
|
||||
pointCount: bucket.points.length,
|
||||
latestCheckedAt: latestPoint?.checkedAt || null,
|
||||
latestCheckedAtMs: latestPoint?.checkedAtMs || 0,
|
||||
latestPrice: latestPoint?.price ?? null,
|
||||
latestDisplayPrice: latestPoint?.displayPrice || null,
|
||||
currency: latestPoint?.currency || 'USD',
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
const aMs = a.latestCheckedAtMs || 0;
|
||||
const bMs = b.latestCheckedAtMs || 0;
|
||||
if (aMs !== bMs) return bMs - aMs;
|
||||
return a.sourceLabel.localeCompare(b.sourceLabel);
|
||||
});
|
||||
|
||||
return {
|
||||
seriesBySource,
|
||||
sourceSummaries,
|
||||
};
|
||||
}
|
||||
|
||||
function getPriceHistoryForOption(option, priceHistoryState) {
|
||||
const optionKeys = getOptionHistoryKeys(option);
|
||||
for (const key of optionKeys) {
|
||||
@@ -237,7 +297,11 @@ function getPriceHistoryForOption(option, priceHistoryState) {
|
||||
|
||||
function decorateOptionWithPriceHistory(option, priceHistoryState) {
|
||||
const priceHistory = getPriceHistoryForOption(option, priceHistoryState);
|
||||
const latestPricePoint = priceHistory.at(-1) || null;
|
||||
const { seriesBySource, sourceSummaries } = buildPriceHistoryBySource(priceHistory);
|
||||
const defaultSourceSummary = sourceSummaries[0] || null;
|
||||
const defaultSourceKey = defaultSourceSummary?.sourceKey || null;
|
||||
const defaultPriceHistory = defaultSourceKey ? seriesBySource[defaultSourceKey] || [] : priceHistory;
|
||||
const latestPricePoint = defaultPriceHistory.at(-1) || null;
|
||||
const optionDetails = toTextList(option.details);
|
||||
const automationHighlights = toTextList(latestPricePoint?.highlights);
|
||||
const automationFeatures = toTextList(latestPricePoint?.features);
|
||||
@@ -255,7 +319,11 @@ function decorateOptionWithPriceHistory(option, priceHistoryState) {
|
||||
|
||||
return {
|
||||
...option,
|
||||
priceHistory,
|
||||
priceHistory: defaultPriceHistory,
|
||||
priceHistoryBySource: seriesBySource,
|
||||
availableSources: sourceSummaries,
|
||||
defaultSourceKey,
|
||||
currentSourceKey: defaultSourceKey,
|
||||
latestPricePoint,
|
||||
currentPrice: latestPricePoint?.price ?? null,
|
||||
decisionDetails: [...new Set(decisionDetails)],
|
||||
|
||||
Reference in New Issue
Block a user