Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions api/app/routers/journey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))],
Expand Down
1 change: 1 addition & 0 deletions api/app/schemas/journey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down
70 changes: 70 additions & 0 deletions api/tests/test_home_current_stop.py
Original file line number Diff line number Diff line change
@@ -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
58 changes: 47 additions & 11 deletions web/src/components/ui/CurrentLocationCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand Down Expand Up @@ -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);
---

<div class="current-airmail-frame" id="current-location">
<article class="current-airmail-card">
<div class="current-grid">
<div class="current-copy">
<div class="current-status">
<span class="pulse-dot"></span>
<div class:list={['current-status', { 'current-status--past': !isLive }]}>
{isLive && <span class="pulse-dot"></span>}
{label && <span class="eyebrow">{label}</span>}
</div>

Expand Down Expand Up @@ -134,13 +162,13 @@ const postmarkState = cityState?.state || '';
)}
</div>

<div class="current-postmark" aria-label={`${primaryTitle} postmark`}>
<div class:list={['current-postmark', { 'current-postmark--past': !isLive }]} aria-label={`${primaryTitle} postmark`}>
<Postmark
city={postmarkCity}
state={postmarkState}
date={stampDate(arrivalDate)}
year={stampYear(arrivalDate)}
landmark={postmarkState || 'On the Road'}
date={stampDate(postmarkDateSource)}
year={stampYear(postmarkDateSource)}
landmark={postmarkLandmark}
size={126}
rotate={-8}
/>
Expand Down Expand Up @@ -227,6 +255,14 @@ const postmarkState = cityState?.state || '';
margin-bottom: 14px;
}

.current-status--past .eyebrow {
color: color-mix(in srgb, var(--muted) 82%, var(--paper) 18%);
}

.current-postmark--past {
opacity: 0.72;
}

.current-title {
margin: 0 0 10px;
color: var(--paper);
Expand Down
35 changes: 35 additions & 0 deletions web/src/lib/stopLive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const DATE_PREFIX_RE = /^(\d{4}-\d{2}-\d{2})/;

function parseStopDate(value) {
if (!value) return null;
const match = typeof value === 'string' && value.match(DATE_PREFIX_RE);
if (match) return match[1];
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return null;
return parsed.toISOString().slice(0, 10);
}

function todayUtcDate() {
return new Date().toISOString().slice(0, 10);
}

/** Mirrors journey._contains_today using UTC calendar dates. */
export function stopContainsToday(stop, today = todayUtcDate()) {
const start = parseStopDate(stop?.start_date);
if (!start) return false;
const end = parseStopDate(stop?.end_date);
if (!end) return start <= today;
return start <= today && today <= end;
}

/** Mirrors journey._is_live_current_stop using UTC calendar dates. */
export function stopIsLive(stop, today = todayUtcDate()) {
if (stopContainsToday(stop, today)) return true;
if (!stop?.is_current) return false;

const start = parseStopDate(stop?.start_date);
if (start && start > today) return false;

const end = parseStopDate(stop?.end_date);
return !end || end >= today;
}
8 changes: 7 additions & 1 deletion web/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import EmptyState from '../components/ui/EmptyState.astro';
import { apiUrl } from '../lib/api.js';
import { formatDate } from '../lib/dates.js';
import { loadSiteText, sectionText } from '../lib/siteText.js';
import { stopIsLive } from '../lib/stopLive.js';

// Kick off the independent site-text fetch up front so it runs concurrently
// with the home-data fetch instead of serially after it.
Expand All @@ -30,6 +31,9 @@ try {
}

const current = homeData?.current_stop;
const currentStopIsLive = typeof homeData?.current_stop_is_live === 'boolean'
? homeData.current_stop_is_live
: (current ? stopIsLive(current) : false);
const activeTrip = homeData?.active_trip_segment;
const nextStop = homeData?.next_stop;
const previousStop = homeData?.previous_stop;
Expand Down Expand Up @@ -156,9 +160,11 @@ function formatStopDate(dateStr) {
placeName={current.place_name}
summary={current.summary}
arrivalDate={current.start_date}
departureDate={current.end_date}
nights={current.nights}
note={current.public_note}
label={currentText.label}
label={currentStopIsLive ? currentText.label : 'Last Stop'}
isLive={currentStopIsLive}
prevStop={prevStopMention}
nextStop={nextStopMention}
weather={weather}
Expand Down
14 changes: 4 additions & 10 deletions web/src/pages/trips/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Layout from '../../layouts/Layout.astro';
import { apiUrl } from '../../lib/api.js';
import { formatDateRange } from '../../lib/dates.js';
import { stopContainsToday, stopIsLive } from '../../lib/stopLive.js';
import MapIsland from '../../components/MapIsland.jsx';
import StopIdentity from '../../components/ui/StopIdentity.astro';
import Breadcrumb from '../../components/ui/Breadcrumb.astro';
Expand Down Expand Up @@ -34,14 +35,7 @@ const stops = trip.stops || [];
const posts = trip.posts || [];

// Identify current stop by date range (mirrors home page API logic)
const todayMs = Date.now();
function containsToday(stop) {
if (!stop.start_date) return false;
const start = new Date(stop.start_date).setHours(0, 0, 0, 0);
const end = stop.end_date ? new Date(stop.end_date).setHours(23, 59, 59, 999) : Infinity;
return start <= todayMs && todayMs <= end;
}
const todayStops = stops.filter(containsToday);
const todayStops = stops.filter(stopContainsToday);
const currentStopId = todayStops.length === 1
? todayStops[0].id
: todayStops.length > 1
Expand All @@ -53,7 +47,7 @@ const tripOgImage = trip.cover_media?.id ? `/media/${trip.cover_media.id}/webp`

const stopsWithCoords = stops
.filter(s => s.latitude != null && s.longitude != null)
.map(s => ({ ...s, is_current: s.is_current || String(s.id) === String(currentStopId) }));
.map(s => ({ ...s, is_current: stopIsLive(s) || String(s.id) === String(currentStopId) }));

const photoCount = posts.reduce((n, p) => n + (p.media?.length || 0), 0);
const milesTotal = stops.reduce((n, s) => n + (s.miles_from_previous || 0), 0);
Expand Down Expand Up @@ -176,7 +170,7 @@ const hasMapKey = Boolean(
<div class="flex items-center gap-2 mb-1 flex-wrap">
{stop.start_date && <span class="label stop-meta">{fmtRange(stop.start_date, stop.end_date)}</span>}
{stop.nights && <span class="label stop-meta">{stop.nights} night{stop.nights !== 1 ? 's' : ''}</span>}
{stop.is_current && <span class="badge badge-active badge-sm">currently here</span>}
{stopIsLive(stop) && <span class="badge badge-active badge-sm">currently here</span>}
</div>
<StopIdentity stop={stop} heading />
</div>
Expand Down
4 changes: 3 additions & 1 deletion web/src/pages/trips/[slug]/stops/[stop].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Layout from '../../../../layouts/Layout.astro';
import { apiUrl } from '../../../../lib/api.js';
import { formatDateRange } from '../../../../lib/dates.js';
import { stopIsLive } from '../../../../lib/stopLive.js';
import { cityStateFromAddress } from '../../../../lib/location.js';
import { mediaOgImage, plainText } from '../../../../lib/og.js';
import CommentsIsland from '../../../../components/CommentsIsland.jsx';
Expand Down Expand Up @@ -61,6 +62,7 @@ function stopSiblingLabel(stop) {
}

const stopTitle = cityStateFromAddress(detail.address_label) || detail.place_name || detail.title;
const stopIsLiveNow = stopIsLive(detail);
const stopDescription = plainText(
detail.summary
|| detail.public_note
Expand Down Expand Up @@ -96,7 +98,7 @@ const hasMapKey = Boolean(
{/* ── Hero ─────────────────────────────────────────────── */}
<section class="mb-12 frame-narrow">
<div class="flex items-center gap-2 mb-3 flex-wrap">
{detail.is_current && <span class="badge badge-active badge-sm">currently here</span>}
{stopIsLiveNow && <span class="badge badge-active badge-sm">currently here</span>}
{detail.start_date && <span class="label">{fmtRange(detail.start_date, detail.end_date)}</span>}
{detail.nights && <span class="label">{detail.nights} night{detail.nights !== 1 ? 's' : ''}</span>}
</div>
Expand Down
Loading