Stream results progressively via SSE instead of waiting for full render

The loading page now opens an EventSource to a new ?render=stream endpoint.
The server immediately sends a shell event (full page chrome: nav, filters,
JS — no external fetches needed), then a section event per direction as each
one's NR + Eurostar data arrives, and finally a done event with the summary
and timetable-refresh URL. The client slots each section card into a
placeholder and calls initialiseResultsPage() only after done, so fares and
advance-fare streaming start at the right moment.

Adds results_shell.html (shell template with empty JS data globals and
mergeSectionData/finaliseResults hooks), results_section.html (extracted
section card partial used by both the full and stream render paths), and
helper functions _section_trip_fares() and _build_summary_html() to avoid
duplicating fare-dict assembly between the two paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-25 21:24:40 +01:00
parent 5f0d2c71b1
commit 453d6244ec
5 changed files with 1182 additions and 197 deletions

View file

@ -0,0 +1,172 @@
<div class="card" style="margin-bottom:1.5rem">
<h2>
{% if section.direction == 'inbound' %}
Return: {{ destination }} &rarr; {{ departure_station_name }}
{% else %}
Outbound: {{ departure_station_name }} &rarr; {{ destination }}
{% endif %}
</h2>
<p class="card-meta">{{ section.date_display }}</p>
{% if section.rows %}
<table class="results-table">
<thead>
<tr>
{% if section.direction == 'inbound' %}
<th class="flow-step">Eurostar<br><span class="text-xs font-normal text-muted">{{ destination }} &rarr; St Pancras</span></th>
<th class="col-transfer flow-step">Transfer<br><span class="text-xs font-normal text-muted">St Pancras &rarr; Paddington</span></th>
<th>National Rail<br><span class="text-xs font-normal text-muted">Paddington &rarr; {{ departure_station_name }}</span></th>
{% else %}
<th class="flow-step">National Rail<br><span class="text-xs font-normal text-muted">{{ departure_station_name }} &rarr; Paddington</span></th>
<th class="col-transfer flow-step">Transfer<br><span class="text-xs font-normal text-muted">Paddington &rarr; St Pancras</span></th>
<th>Eurostar<br><span class="text-xs font-normal text-muted">St Pancras &rarr; {{ destination }}</span></th>
{% endif %}
<th class="nowrap">Total<br><span class="text-xs font-normal text-muted">click row to select</span></th>
</tr>
</thead>
<tbody>
{% set trip_rows = section.rows | selectattr('row_type', 'equalto', 'trip') | list %}
{% if trip_rows %}
{% set best_mins = trip_rows | map(attribute='total_minutes') | min %}
{% set worst_mins = trip_rows | map(attribute='total_minutes') | max %}
{% endif %}
{% for row in section.rows %}
{% if row.row_type == 'trip' and row.total_minutes <= best_mins + 5 and trip_rows | length > 1 %}
{% set row_class = 'row-fast' %}
{% elif row.row_type == 'trip' and row.total_minutes >= worst_mins - 5 and trip_rows | length > 1 %}
{% set row_class = 'row-slow' %}
{% elif row.row_type == 'unreachable' %}
{% set row_class = 'row-unreachable' %}
{% elif loop.index is odd %}
{% set row_class = 'row-alt' %}
{% else %}
{% set row_class = '' %}
{% endif %}
<tr class="{{ row_class }}{% if row.row_type == 'trip' %} row-selectable{% endif %}"
data-row-key="{{ row.row_key }}"
{% if row.eurostar_price is not none %}data-es-std="{{ row.eurostar_price }}"{% endif %}
{% if row.row_type == 'trip' %}onclick="selectRow(this)"{% endif %}>
{% if row.row_type == 'trip' %}
{% if section.direction == 'inbound' %}
<td>
<span class="font-bold nowrap"><span class="font-normal text-muted" style="font-size:0.85em">(CET)</span> {{ row.depart_destination }} &rarr; {{ row.arrive_st_pancras }} <span class="font-normal text-muted" style="font-size:0.85em">(UK)</span></span>
<br><span class="text-xs text-muted">check in by {{ row.check_in_by }}</span>
{% if row.eurostar_duration or row.train_number %}
<br><span class="text-xs text-muted">
{%- if row.eurostar_duration %}<span class="nowrap">({{ row.eurostar_duration }})</span>{% endif %}
{%- if row.eurostar_duration and row.train_number %} &middot; {% endif %}
{%- if row.train_number %}{{ row.train_number }}{% endif %}
</span>
{% endif %}
<span class="fare-line es-standard"><span class="text-xs text-muted">Std</span>{% if row.eurostar_price is not none %} <span class="text-sm font-bold">£{{ "%.2f"|format(row.eurostar_price) }}</span>{% endif %}</span>
<span class="fare-line es-plus"><span class="text-xs text-muted">SP</span>{% if row.eurostar_plus_price is not none %} <span class="text-sm font-bold">£{{ "%.2f"|format(row.eurostar_plus_price) }}</span>{% endif %}</span>
<span class="text-xs text-muted mobile-conn">then {{ row.connection_duration }} to station{% if row.connection_minutes < 45 %} {% endif %}</span>
</td>
<td class="col-transfer" style="color:#4a5568">
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 45 %} <span title="Tight connection">⚠️</span>{% endif %}</span>
{% if row.circle_services %}
{% if row.circle_services | length > 1 %}
{% set c_early = row.circle_services[0] %}
{% set c = row.circle_services[1] %}
<br><span class="text-xs text-muted nowrap">Circle {{ c_early.depart }} &rarr; PAD {{ c_early.arrive_pad }} · £{{ "%.2f"|format(c_early.fare) }}</span>
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">next {{ c.depart }} &rarr; PAD {{ c.arrive_pad }}</span>
{% else %}
{% set c = row.circle_services[0] %}
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} &rarr; PAD {{ c.arrive_pad }} · £{{ "%.2f"|format(c.fare) }}</span>
{% endif %}
{% endif %}
</td>
<td>
<span class="font-bold nowrap">{{ row.depart_paddington }} &rarr; {{ row.arrive_uk_station }}</span>
<span class="text-sm text-muted nowrap">({{ row.gwr_duration }})</span>
{% if row.headcode or row.arrive_platform %}
<br><span class="text-xs text-muted mobile-hide">{{ row.headcode }}{% if row.headcode and row.arrive_platform %} &middot; {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}</span>
{% endif %}
<span class="fare-line nr-walkon">{% if row.ticket_price is not none %}<span class="text-sm font-bold">£{{ "%.2f"|format(row.ticket_price) }}</span>{% endif %}</span>
<span class="fare-line nr-advance-std"></span>
<span class="fare-line nr-advance-1st"></span>
</td>
{% else %}
<td>
<span class="font-bold nowrap">{{ row.depart_bristol }} &rarr; {{ row.arrive_paddington }}</span>
<span class="text-sm text-muted nowrap">({{ row.gwr_duration }})</span>
{% if row.headcode or row.arrive_platform %}
<br><span class="text-xs text-muted mobile-hide">{{ row.headcode }}{% if row.headcode and row.arrive_platform %} &middot; {% endif %}{% if row.arrive_platform %}Plat {{ row.arrive_platform }}{% endif %}</span>
{% endif %}
<span class="fare-line nr-walkon">{% if row.ticket_price is not none %}<span class="text-sm font-bold">£{{ "%.2f"|format(row.ticket_price) }}</span>{% endif %}</span>
<span class="fare-line nr-advance-std"></span>
<span class="fare-line nr-advance-1st"></span>
<span class="text-xs text-muted mobile-conn">then {{ row.connection_duration }} to Eurostar{% if row.connection_minutes < 80 %} {% endif %}</span>
</td>
<td class="col-transfer" style="color:#4a5568">
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 80 %} <span title="Tight connection">⚠️</span>{% endif %}</span>
{% if row.circle_services %}
{% set c = row.circle_services[0] %}
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} &rarr; KX {{ c.arrive_kx }} · £{{ "%.2f"|format(c.fare) }}</span>
{% if row.circle_services | length > 1 %}
{% set c2 = row.circle_services[1] %}
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">next {{ c2.depart }} &rarr; KX {{ c2.arrive_kx }} · £{{ "%.2f"|format(c2.fare) }}</span>
{% endif %}
{% endif %}
</td>
<td>
<span class="font-bold nowrap">{{ row.depart_st_pancras }} &rarr; {{ row.arrive_destination }} <span class="font-normal text-muted" style="font-size:0.85em">(CET)</span></span>
{% if row.eurostar_duration or row.train_number %}
<br><span class="text-xs text-muted">
{%- if row.eurostar_duration %}<span class="nowrap">({{ row.eurostar_duration }})</span>{% endif %}
{%- if row.eurostar_duration and row.train_number %} &middot; {% endif %}
{%- if row.train_number %}{{ row.train_number }}{% endif %}
</span>
{% endif %}
<span class="fare-line es-standard"><span class="text-xs text-muted">Std</span>{% if row.eurostar_price is not none %} <span class="text-sm font-bold">£{{ "%.2f"|format(row.eurostar_price) }}</span>{% endif %}</span>
<span class="fare-line es-plus"><span class="text-xs text-muted">SP</span>{% if row.eurostar_plus_price is not none %} <span class="text-sm font-bold">£{{ "%.2f"|format(row.eurostar_plus_price) }}</span>{% endif %}</span>
</td>
{% endif %}
<td class="font-bold nowrap">
{% if row.total_minutes <= best_mins + 5 and trip_rows | length > 1 %}
<span class="text-green" title="Fastest journey">{{ row.total_duration }} ⚡</span>
{% elif row.total_minutes >= worst_mins - 5 and trip_rows | length > 1 %}
<span class="text-red" title="Slowest journey">{{ row.total_duration }} 🐢</span>
{% else %}
<span class="text-blue">{{ row.total_duration }}</span>
{% endif %}
<br><span class="total-price"></span>
</td>
{% else %}
<td>
<span class="text-dimmed text-sm" title="No National Rail service from {{ departure_station_name }} arrives at Paddington in time for this Eurostar">Too early</span>
</td>
<td class="col-transfer text-dimmed">&mdash;</td>
<td>
{% if section.direction == 'inbound' %}
<span class="font-bold nowrap text-dimmed">{{ row.depart_destination }} &rarr; {{ row.arrive_st_pancras }}</span>
{% if row.train_number %}<br><span class="text-xs text-dimmed">{{ row.train_number }}</span>{% endif %}
{% else %}
<span class="font-bold nowrap text-dimmed">{{ row.depart_st_pancras }} &rarr; {{ row.arrive_destination }}</span>
{% if row.train_number %}<br><span class="text-xs text-dimmed">{{ row.train_number }}</span>{% endif %}
{% endif %}
<span class="fare-line es-standard"></span>
<span class="fare-line es-plus"></span>
</td>
<td class="text-dimmed nowrap"><span class="total-price"></span></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>No valid journeys found.</p>
<p>
{% if section.gwr_count == 0 and section.eurostar_count == 0 %}
Could not retrieve train data. Check your network connection or try again.
{% elif section.gwr_count == 0 %}
No National Rail trains found for this date.
{% elif section.eurostar_count == 0 %}
No Eurostar services found for {{ destination }} on this date.
{% else %}
No National Rail + Eurostar combination has a {{ section.min_connection }}-{{ section.max_connection }} minute connection.
{% endif %}
</p>
</div>
{% endif %}
</div>