diff --git a/src/app/__tests__/unit/weatherService.test.ts b/src/app/__tests__/unit/weatherService.test.ts
index b692f539..74ea1a60 100644
--- a/src/app/__tests__/unit/weatherService.test.ts
+++ b/src/app/__tests__/unit/weatherService.test.ts
@@ -76,3 +76,88 @@ describe('weatherService negative cache helpers', () => {
}, now)).toBe(false);
});
});
+
+describe('weatherService legacy cache normalization', () => {
+ const legacyDay = (date: string) => ({
+ id: `weather-${date}`,
+ date,
+ coordinates: [47.5, 19] as [number, number],
+ temperature: { min: 10, max: 20, average: 15 },
+ precipitation: { total: 0, probability: null },
+ wind: { speed: null, direction: null },
+ conditions: { description: 'Clear', icon: 'sun', code: 0, cloudCover: null, humidity: null },
+ fetchedAt: '2026-06-01T00:00:00.000Z'
+ });
+
+ it('marks all-future legacy cache entries as historical averages', () => {
+ const normalized = weatherServiceTestUtils.normalizeCachedWeatherSummary({
+ ...emptySummary,
+ startDate: '2027-06-01',
+ endDate: '2027-06-02',
+ dailyWeather: [
+ legacyDay('2027-06-01'),
+ legacyDay('2027-06-02')
+ ] as WeatherSummary['dailyWeather']
+ }, new Date('2026-06-01T12:00:00.000Z'));
+
+ expect(normalized.dailyWeather).toEqual(expect.arrayContaining([
+ expect.objectContaining({
+ date: '2027-06-01',
+ dataSource: 'historical-average',
+ isHistorical: true,
+ isForecast: false
+ }),
+ expect.objectContaining({
+ date: '2027-06-02',
+ dataSource: 'historical-average',
+ isHistorical: true,
+ isForecast: false
+ })
+ ]));
+ });
+
+ it('infers recorded open-meteo data for legacy cache entries that include past days', () => {
+ const normalized = weatherServiceTestUtils.normalizeCachedWeatherSummary({
+ ...emptySummary,
+ startDate: '2026-05-30',
+ endDate: '2026-06-01',
+ dailyWeather: [
+ legacyDay('2026-05-30'),
+ legacyDay('2026-06-01')
+ ] as WeatherSummary['dailyWeather']
+ }, new Date('2026-06-01T12:00:00.000Z'));
+
+ expect(normalized.dailyWeather).toEqual(expect.arrayContaining([
+ expect.objectContaining({
+ date: '2026-05-30',
+ dataSource: 'open-meteo',
+ isHistorical: true,
+ isForecast: false
+ }),
+ expect.objectContaining({
+ date: '2026-06-01',
+ dataSource: 'open-meteo',
+ isHistorical: true,
+ isForecast: false
+ })
+ ]));
+ });
+
+ it('fills missing forecast and historical flags when cached data has a source', () => {
+ const normalized = weatherServiceTestUtils.normalizeCachedWeatherSummary({
+ ...emptySummary,
+ startDate: '2026-06-01',
+ endDate: '2026-06-02',
+ dailyWeather: [
+ { ...legacyDay('2026-06-02'), dataSource: 'open-meteo' }
+ ] as WeatherSummary['dailyWeather']
+ }, new Date('2026-06-01T12:00:00.000Z'));
+
+ expect(normalized.dailyWeather[0]).toEqual(expect.objectContaining({
+ date: '2026-06-02',
+ dataSource: 'open-meteo',
+ isHistorical: false,
+ isForecast: true
+ }));
+ });
+});
diff --git a/src/app/components/Weather/WeatherSummary.tsx b/src/app/components/Weather/WeatherSummary.tsx
index c776a1b5..1f964f1b 100644
--- a/src/app/components/Weather/WeatherSummary.tsx
+++ b/src/app/components/Weather/WeatherSummary.tsx
@@ -26,6 +26,13 @@ export default function WeatherSummary({ summary }: Props) {
const hasHistAvg = sourceDays.some(d => d.dataSource === 'historical-average');
const hasForecast = sourceDays.some(d => d.isForecast);
const hasRecorded = sourceDays.some(d => d.isHistorical && d.dataSource !== 'historical-average');
+ const hasExplicitSourceMetadata = sourceDays.some(d =>
+ d.dataSource === 'historical-average' ||
+ d.dataSource === 'open-meteo' ||
+ d.dataSource === 'cache' ||
+ typeof d.isForecast === 'boolean' ||
+ typeof d.isHistorical === 'boolean'
+ );
if (hasHistAvg && hasForecast && hasRecorded) return 'Recorded + Forecast + Hist. avg.';
if (hasHistAvg && hasForecast) return 'Forecast + Hist. avg.';
@@ -34,6 +41,9 @@ export default function WeatherSummary({ summary }: Props) {
if (hasHistAvg) return 'Hist. avg.';
if (hasForecast) return 'Forecast';
if (hasRecorded) return 'Recorded';
+ if (!hasExplicitSourceMetadata) {
+ return sourceDays.every(d => d.date > todayISO) ? 'Hist. avg.' : 'Recorded';
+ }
return '';
})();
return (
diff --git a/src/app/components/Weather/__tests__/WeatherSummary.test.tsx b/src/app/components/Weather/__tests__/WeatherSummary.test.tsx
new file mode 100644
index 00000000..4f87e8be
--- /dev/null
+++ b/src/app/components/Weather/__tests__/WeatherSummary.test.tsx
@@ -0,0 +1,54 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import WeatherSummary from '@/app/components/Weather/WeatherSummary';
+import type { WeatherSummary as WeatherSummaryType } from '@/app/types/weather';
+
+const buildLegacySummary = (dates: string[]): WeatherSummaryType => ({
+ locationId: 'location-1',
+ startDate: dates[0],
+ endDate: dates[dates.length - 1],
+ dailyWeather: dates.map(date => ({
+ id: `weather-${date}`,
+ date,
+ coordinates: [47.5, 19] as [number, number],
+ temperature: { min: 10, max: 20, average: 15 },
+ precipitation: { total: 0, probability: null },
+ wind: { speed: null, direction: null },
+ conditions: { description: 'Clear', icon: 'sun', code: 0, cloudCover: null, humidity: null },
+ fetchedAt: '2026-06-01T00:00:00.000Z',
+ })) as WeatherSummaryType['dailyWeather'],
+ summary: {
+ averageTemp: 15,
+ totalPrecipitation: 0,
+ predominantCondition: 'Clear',
+ },
+});
+
+describe('WeatherSummary', () => {
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('labels all-future legacy cached weather as historical averages', () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2026-06-01T12:00:00.000Z'));
+
+ render();
+
+ expect(screen.getByText('Weather · Hist. avg.')).toBeInTheDocument();
+ });
+
+ it('labels past legacy cached weather as recorded instead of rendering a blank source label', () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2026-06-01T12:00:00.000Z'));
+
+ render();
+
+ expect(screen.getByText('Weather · Recorded')).toBeInTheDocument();
+ });
+});
diff --git a/src/app/services/weatherService.ts b/src/app/services/weatherService.ts
index eff6e802..262f98ee 100644
--- a/src/app/services/weatherService.ts
+++ b/src/app/services/weatherService.ts
@@ -282,7 +282,7 @@ async function readCache(key: string): Promise {
const exp = parseISO(d.expiresAt);
return isValid(exp) && isAfter(exp, today);
});
- const summary: WeatherSummary = { ...parsed.summary, dailyWeather: filtered };
+ const summary = normalizeCachedWeatherSummary({ ...parsed.summary, dailyWeather: filtered });
const fetchedAt = typeof parsed.fetchedAt === 'string' ? parsed.fetchedAt : LEGACY_FETCHED_AT;
const entry: CacheEntry = {
key: parsed.key || key,
@@ -346,6 +346,48 @@ function computeSources(daily: WeatherData[]): CacheSources {
return result;
}
+function isKnownWeatherDataSource(value: unknown): value is WeatherData['dataSource'] {
+ return value === 'open-meteo' || value === 'cache' || value === 'historical-average';
+}
+
+function normalizeCachedWeatherSummary(summary: WeatherSummary, today: Date = new Date()): WeatherSummary {
+ const todayISO = toISODate(today);
+ const hasExplicitSourceMetadata = summary.dailyWeather.some(day => {
+ const rawDay = day as Partial;
+ return (
+ isKnownWeatherDataSource(rawDay.dataSource) ||
+ typeof rawDay.isForecast === 'boolean' ||
+ typeof rawDay.isHistorical === 'boolean'
+ );
+ });
+ const legacyAllFuture = !hasExplicitSourceMetadata && summary.dailyWeather.every(day => day.date > todayISO);
+
+ return {
+ ...summary,
+ dailyWeather: summary.dailyWeather.map(day => {
+ const rawDay = day as Partial;
+ const dataSource = isKnownWeatherDataSource(rawDay.dataSource)
+ ? rawDay.dataSource
+ : legacyAllFuture
+ ? 'historical-average'
+ : 'open-meteo';
+ const isHistorical = typeof rawDay.isHistorical === 'boolean'
+ ? rawDay.isHistorical
+ : dataSource === 'historical-average' || day.date <= todayISO;
+ const isForecast = typeof rawDay.isForecast === 'boolean'
+ ? rawDay.isForecast
+ : dataSource === 'open-meteo' && !isHistorical;
+
+ return {
+ ...day,
+ dataSource,
+ isHistorical,
+ isForecast,
+ };
+ }),
+ };
+}
+
function enumerateDateStrings(startISO: string, endISO: string): string[] {
const start = parseISO(startISO);
const end = parseISO(endISO);
@@ -844,6 +886,7 @@ export const weatherServiceTestUtils = {
getRateLimitHeaderDelayMs,
needsForecastRefresh,
isFreshEmptyCacheEntry,
+ normalizeCachedWeatherSummary,
resetRateLimitStateForTests
};