diff --git a/api/app/routers/journey.py b/api/app/routers/journey.py index 119b05a..6da6cdd 100644 --- a/api/app/routers/journey.py +++ b/api/app/routers/journey.py @@ -120,14 +120,28 @@ def _is_visible_stop_status(stop: Stop) -> bool: def _contains_today(stop: Stop, today) -> bool: start = stop.start_date.date() if stop.start_date else None - end = stop.end_date.date() if stop.end_date else start if not start: return False - if end is None: - end = start + if not stop.end_date: + return start <= today + end = stop.end_date.date() return start <= today <= end +def _is_live_current_stop(stop: Stop, today) -> bool: + if _contains_today(stop, today): + return True + # Honor stale is_current only when the stop hasn't clearly ended. + if stop.is_current: + start = stop.start_date.date() if stop.start_date else None + if start and start > today: + return False + end = stop.end_date.date() if stop.end_date else None + if end is None or end >= today: + return True + return False + + async def _coordinates_for_stops(session: AsyncSession, stops) -> dict[uuid.UUID, tuple[float, float]]: stop_ids = [stop.id for stop in stops] if not stop_ids: @@ -351,9 +365,13 @@ async def get_home( has_more = has_more_posts or len(past_stops) > 5 weather = await _cached_weather_and_publish_coords(current_stop) + current_stop_is_live = ( + _is_live_current_stop(current_stop_model, today) if current_stop_model else False + ) return HomeOut( current_stop=current_stop, + current_stop_is_live=current_stop_is_live, next_stop=_stop_out(next_stop_model, coords, user), previous_stop=_stop_out(previous_stop_model, coords, user), recent_stops=[stop_out for s in recent_stop_models if (stop_out := _stop_out(s, coords, user))], diff --git a/api/app/schemas/journey.py b/api/app/schemas/journey.py index f2b23ec..e62e35e 100644 --- a/api/app/schemas/journey.py +++ b/api/app/schemas/journey.py @@ -138,6 +138,7 @@ class WeatherOut(BaseModel): class HomeOut(BaseModel): current_stop: Optional[PublicStopSummary] = None + current_stop_is_live: bool = False next_stop: Optional[PublicStopSummary] = None previous_stop: Optional[PublicStopSummary] = None recent_stops: List[PublicStopSummary] = [] diff --git a/api/tests/test_home_current_stop.py b/api/tests/test_home_current_stop.py new file mode 100644 index 0000000..ae7c2d8 --- /dev/null +++ b/api/tests/test_home_current_stop.py @@ -0,0 +1,70 @@ +from datetime import date, datetime, timezone +from types import SimpleNamespace + +from app.routers.journey import _contains_today, _is_live_current_stop + + +def _stop(*, start, end=None, is_current=False): + return SimpleNamespace( + start_date=datetime.combine(start, datetime.min.time(), tzinfo=timezone.utc), + end_date=datetime.combine(end, datetime.min.time(), tzinfo=timezone.utc) if end else None, + is_current=is_current, + ) + + +def test_live_when_stop_date_range_includes_today(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 15), end=date(2026, 8, 20)) + + assert _is_live_current_stop(stop, today) is True + + +def test_not_live_for_past_stop_without_is_current(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 9), end=date(2026, 8, 12)) + + assert _is_live_current_stop(stop, today) is False + + +def test_not_live_for_past_stop_with_stale_is_current(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 9), end=date(2026, 8, 12), is_current=True) + + assert _is_live_current_stop(stop, today) is False + + +def test_live_for_open_ended_stop_with_is_current(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 10), is_current=True) + + assert _is_live_current_stop(stop, today) is True + + +def test_live_for_open_ended_stop_without_is_current(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 10)) + + assert _contains_today(stop, today) is True + assert _is_live_current_stop(stop, today) is True + + +def test_not_live_for_future_stop(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 20), end=date(2026, 8, 25)) + + assert _is_live_current_stop(stop, today) is False + + +def test_not_live_for_future_stop_with_stale_is_current(): + today = date(2026, 8, 17) + stop = _stop(start=date(2026, 8, 20), end=date(2026, 8, 25), is_current=True) + + assert _is_live_current_stop(stop, today) is False + + +def test_live_on_end_date(): + today = date(2026, 8, 13) + stop = _stop(start=date(2026, 8, 9), end=date(2026, 8, 13)) + + assert _contains_today(stop, today) is True + assert _is_live_current_stop(stop, today) is True diff --git a/web/src/components/ui/CurrentLocationCard.astro b/web/src/components/ui/CurrentLocationCard.astro index 6f4abb0..f862a13 100644 --- a/web/src/components/ui/CurrentLocationCard.astro +++ b/web/src/components/ui/CurrentLocationCard.astro @@ -35,27 +35,53 @@ interface Props { nights?: number; note?: string; label?: string; + isLive?: boolean; + departureDate?: string; prevStop?: StopMention; nextStop?: StopMention; weather?: Weather | null; } -const { stopTitle, stopSlug, tripTitle, tripSlug, placeName, summary, arrivalDate, nights, note, label = 'Currently At', prevStop, nextStop, weather } = Astro.props; +const { + stopTitle, + stopSlug, + tripTitle, + tripSlug, + placeName, + summary, + arrivalDate, + nights, + note, + label = 'Currently At', + isLive = false, + departureDate, + prevStop, + nextStop, + weather, +} = Astro.props; function formatDate(dateStr?: string) { if (!dateStr) return ''; return fmtDate(dateStr, { weekday: 'short', month: 'short', day: 'numeric' }) || dateStr; } +function dateOnlyStampSource(dateStr?: string) { + if (!dateStr) return undefined; + const match = dateStr.match(/^(\d{4}-\d{2}-\d{2})/); + return match ? match[1] : dateStr; +} + function stampDate(dateStr?: string) { - if (!dateStr) return 'ON ROAD'; - return fmtDate(dateStr, { month: 'short', day: 'numeric' }) || dateStr; + const normalized = dateOnlyStampSource(dateStr); + if (!normalized) return 'ON ROAD'; + return fmtDate(normalized, { month: 'short', day: 'numeric' }) || normalized; } function stampYear(dateStr?: string) { - if (!dateStr) return ''; + const normalized = dateOnlyStampSource(dateStr); + if (!normalized) return ''; try { - return String(new Date(dateStr).getFullYear()); + return normalized.slice(0, 4); } catch { return ''; } @@ -89,14 +115,16 @@ const primaryTitle = cityState?.label || stopTitle; const secondaryTitle = cityState ? stopTitle : placeName && placeName !== stopTitle ? placeName : ''; const postmarkCity = cityState?.city || stopTitle; const postmarkState = cityState?.state || ''; +const postmarkLandmark = isLive ? (postmarkState || 'On the Road') : 'Last Stop'; +const postmarkDateSource = isLive ? arrivalDate : (departureDate || arrivalDate); ---