Forecasting app: hybrid port to HNF stack

Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 18:49:34 +00:00
commit 75d2c1fa9d
103 changed files with 70316 additions and 0 deletions

View file

@ -0,0 +1,161 @@
"""
Time alignment utilities for prior year comparisons.
Ensures consistent comparison logic across the application:
- Daily: 364 days (52 weeks) for day-of-week alignment (MonMon, SatSat)
- Weekly: ISO week number matching (Week 6 2026 vs Week 6 2025)
- Monthly: Same month, prior year
This module should be used whenever comparing to prior year data.
"""
from datetime import date, timedelta
from typing import Tuple, Optional
def get_prior_year_daily(target_date: date) -> date:
"""
Get the comparable date from prior year for daily comparisons.
Uses 364 days (exactly 52 weeks) to ensure day-of-week alignment:
- Monday Monday
- Saturday Saturday
Example:
Wed 11 Feb 2026 Wed 12 Feb 2025 (not 11 Feb 2025 which was a different day)
Args:
target_date: The date to find comparison for
Returns:
The prior year date with same day of week
"""
return target_date - timedelta(days=364)
def get_prior_year_weekly(target_date: date) -> Tuple[int, int]:
"""
Get the ISO week and year for weekly year-over-year comparison.
Uses ISO week numbers so Week 6 of 2026 compares to Week 6 of 2025.
This ensures full weeks are compared (Mon-Sun) regardless of calendar dates.
Args:
target_date: Any date within the target week
Returns:
Tuple of (year, week_number) for the comparison week
"""
iso_cal = target_date.isocalendar()
return (iso_cal.year - 1, iso_cal.week)
def get_prior_year_week_dates(target_date: date) -> Tuple[date, date]:
"""
Get the start and end dates of the same ISO week in the prior year.
Useful for querying data for the entire comparison week.
Args:
target_date: Any date within the target week
Returns:
Tuple of (week_start, week_end) for the prior year's matching week
"""
iso_cal = target_date.isocalendar()
prior_year = iso_cal.year - 1
prior_week = iso_cal.week
# Handle edge case: if prior year doesn't have this week number
# (can happen with week 53), fall back to last week of prior year
try:
# Find the first day of the target week in prior year
# Week 1 day 1 of the prior year
jan_1_prior = date(prior_year, 1, 1)
jan_1_iso = jan_1_prior.isocalendar()
# Calculate days to add to get to the target week
# First, get to week 1 day 1
days_to_week_1 = (1 - jan_1_iso.weekday) % 7
week_1_monday = jan_1_prior + timedelta(days=days_to_week_1)
# Adjust if Jan 1 is in the previous year's last week
if jan_1_iso.week != 1:
week_1_monday = jan_1_prior + timedelta(days=(7 - jan_1_prior.weekday()))
# Now add weeks to get to target week
week_start = week_1_monday + timedelta(weeks=prior_week - 1)
# Verify we got the right week
if week_start.isocalendar().week != prior_week:
# Fallback: use last week of prior year
dec_28_prior = date(prior_year, 12, 28) # Always in last week
dec_28_iso = dec_28_prior.isocalendar()
week_start = dec_28_prior - timedelta(days=dec_28_prior.weekday())
week_end = week_start + timedelta(days=6)
return (week_start, week_end)
except (ValueError, AttributeError):
# Fallback to simpler calculation
prior_date = get_prior_year_daily(target_date)
week_start = prior_date - timedelta(days=prior_date.weekday())
week_end = week_start + timedelta(days=6)
return (week_start, week_end)
def get_prior_year_monthly(target_date: date) -> Tuple[date, date]:
"""
Get the start and end dates of the same month in the prior year.
Args:
target_date: Any date within the target month
Returns:
Tuple of (month_start, month_end) for the prior year's matching month
"""
prior_year = target_date.year - 1
month = target_date.month
# First day of the month
month_start = date(prior_year, month, 1)
# Last day of the month
if month == 12:
month_end = date(prior_year + 1, 1, 1) - timedelta(days=1)
else:
month_end = date(prior_year, month + 1, 1) - timedelta(days=1)
return (month_start, month_end)
# SQL helper constants for use in queries
SQL_PRIOR_YEAR_DAILY = "INTERVAL '364 days'" # For daily comparisons
SQL_PRIOR_YEAR_OFFSET = 364 # Days offset for daily DOW alignment
def get_comparison_info(target_date: date) -> dict:
"""
Get formatted comparison information for display.
Useful for showing users what date/week is being compared.
Args:
target_date: The date being forecasted/analyzed
Returns:
Dict with comparison details
"""
prior_daily = get_prior_year_daily(target_date)
prior_week_year, prior_week_num = get_prior_year_weekly(target_date)
day_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
return {
"target_date": target_date,
"target_day": day_names[target_date.weekday()],
"target_iso_week": target_date.isocalendar().week,
"prior_year_date": prior_daily,
"prior_year_day": day_names[prior_daily.weekday()], # Should match target_day
"prior_year_iso_week": prior_week_num,
"comparison_note": f"vs {day_names[prior_daily.weekday()]} {prior_daily.strftime('%d %b %Y')}"
}