fix: return FT components directly for proper toast injection

POST routes were returning HTMLResponse(content=to_xml(...)) which
bypassed FastHTML's toast middleware. The middleware only injects
toasts for tuple, FT, or FtResponse responses.

Changed 12 routes to return render_page() directly:
- actions.py: 7 routes (cohort, hatch, tag-add, tag-end, attrs, outcome, status-correct)
- eggs.py: 2 routes (product-collected, product-sold)
- feed.py: 2 routes (feed-given, feed-purchased)
- move.py: 1 route (animal-move)

Updated tests to check for toast content in response body instead of
session cookie, since middleware now renders toasts inline.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-02 20:11:05 +00:00
parent 628d5cc6e6
commit 9fbda655f5
6 changed files with 120 additions and 224 deletions

View File

@@ -206,15 +206,11 @@ async def animal_cohort(request: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( cohort_form(locations, species_list),
request, title="Create Cohort - AnimalTrack",
cohort_form(locations, species_list), active_nav=None,
title="Create Cohort - AnimalTrack",
active_nav=None,
)
),
) )
@@ -349,15 +345,11 @@ async def hatch_recorded(request: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( hatch_form(locations, species_list),
request, title="Record Hatch - AnimalTrack",
hatch_form(locations, species_list), active_nav=None,
title="Record Hatch - AnimalTrack",
active_nav=None,
)
),
) )
@@ -694,15 +686,11 @@ async def animal_tag_add(request: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( tag_add_form(),
request, title="Add Tag - AnimalTrack",
tag_add_form(), active_nav=None,
title="Add Tag - AnimalTrack",
active_nav=None,
)
),
) )
@@ -947,15 +935,11 @@ async def animal_tag_end(request: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( tag_end_form(),
request, title="End Tag - AnimalTrack",
tag_end_form(), active_nav=None,
title="End Tag - AnimalTrack",
active_nav=None,
)
),
) )
@@ -1183,15 +1167,11 @@ async def animal_attrs(request: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( attrs_form(),
request, title="Update Attributes - AnimalTrack",
attrs_form(), active_nav=None,
title="Update Attributes - AnimalTrack",
active_nav=None,
)
),
) )
@@ -1468,22 +1448,18 @@ async def animal_outcome(request: Request, session):
product_repo = ProductRepository(db) product_repo = ProductRepository(db)
products = [(p.code, p.name) for p in product_repo.list_all() if p.active] products = [(p.code, p.name) for p in product_repo.list_all() if p.active]
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( outcome_form(
request, filter_str="",
outcome_form( resolved_ids=[],
filter_str="", roster_hash="",
resolved_ids=[], ts_utc=int(time.time() * 1000),
roster_hash="", resolved_count=0,
ts_utc=int(time.time() * 1000), products=products,
resolved_count=0,
products=products,
),
title="Record Outcome - AnimalTrack",
active_nav=None,
)
), ),
title="Record Outcome - AnimalTrack",
active_nav=None,
) )
@@ -1692,21 +1668,17 @@ async def animal_status_correct(req: Request, session):
) )
# Success: re-render fresh form # Success: re-render fresh form
return HTMLResponse( return render_page(
content=to_xml( req,
render_page( status_correct_form(
req, filter_str="",
status_correct_form( resolved_ids=[],
filter_str="", roster_hash="",
resolved_ids=[], ts_utc=int(time.time() * 1000),
roster_hash="", resolved_count=0,
ts_utc=int(time.time() * 1000),
resolved_count=0,
),
title="Correct Status - AnimalTrack",
active_nav=None,
)
), ),
title="Correct Status - AnimalTrack",
active_nav=None,
) )

View File

@@ -209,22 +209,18 @@ async def product_collected(request: Request, session):
) )
# Success: re-render form with location sticking, qty cleared # Success: re-render form with location sticking, qty cleared
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( eggs_page(
request, locations,
eggs_page( products,
locations, active_tab="harvest",
products, selected_location_id=location_id,
active_tab="harvest", harvest_action=product_collected,
selected_location_id=location_id, sell_action=product_sold,
harvest_action=product_collected,
sell_action=product_sold,
),
title="Eggs - AnimalTrack",
active_nav="eggs",
)
), ),
title="Eggs - AnimalTrack",
active_nav="eggs",
) )
@@ -320,22 +316,18 @@ async def product_sold(request: Request, session):
) )
# Success: re-render form with product sticking # Success: re-render form with product sticking
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( eggs_page(
request, locations,
eggs_page( products,
locations, active_tab="sell",
products, selected_product_code=product_code,
active_tab="sell", harvest_action=product_collected,
selected_product_code=product_code, sell_action=product_sold,
harvest_action=product_collected,
sell_action=product_sold,
),
title="Eggs - AnimalTrack",
active_nav="eggs",
)
), ),
title="Eggs - AnimalTrack",
active_nav="eggs",
) )

View File

@@ -245,25 +245,21 @@ async def feed_given(request: Request, session):
) )
# Success: re-render form with location/type sticking, amount reset # Success: re-render form with location/type sticking, amount reset
return HTMLResponse( return render_page(
content=str( request,
render_page( feed_page(
request, locations,
feed_page( feed_types,
locations, active_tab="give",
feed_types, selected_location_id=location_id,
active_tab="give", selected_feed_type_code=feed_type_code,
selected_location_id=location_id, default_amount_kg=default_amount_kg,
selected_feed_type_code=feed_type_code, balance_warning=balance_warning,
default_amount_kg=default_amount_kg, give_action=feed_given,
balance_warning=balance_warning, purchase_action=feed_purchased,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
), ),
title="Feed - AnimalTrack",
active_nav="feed",
) )
@@ -404,21 +400,17 @@ async def feed_purchased(request: Request, session):
) )
# Success: re-render form with fields cleared # Success: re-render form with fields cleared
return HTMLResponse( return render_page(
content=str( request,
render_page( feed_page(
request, locations,
feed_page( feed_types,
locations, active_tab="purchase",
feed_types, give_action=feed_given,
active_tab="purchase", purchase_action=feed_purchased,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
), ),
title="Feed - AnimalTrack",
active_nav="feed",
) )

View File

@@ -299,18 +299,14 @@ async def animal_move(request: Request, session):
) )
# Success: re-render fresh form (nothing sticks per spec) # Success: re-render fresh form (nothing sticks per spec)
return HTMLResponse( return render_page(
content=to_xml( request,
render_page( move_form(
request, locations,
move_form( action=animal_move,
locations,
action=animal_move,
),
title="Move - AnimalTrack",
active_nav="move",
)
), ),
title="Move - AnimalTrack",
active_nav="move",
) )

View File

@@ -149,7 +149,7 @@ class TestCohortCreationSuccess:
assert count_after == count_before + 3 assert count_after == count_before + 3
def test_cohort_success_returns_toast(self, client, seeded_db, location_strip1_id): def test_cohort_success_returns_toast(self, client, seeded_db, location_strip1_id):
"""Successful cohort creation stores toast in session.""" """Successful cohort creation renders toast in response body."""
resp = client.post( resp = client.post(
"/actions/animal-cohort", "/actions/animal-cohort",
data={ data={
@@ -164,20 +164,8 @@ class TestCohortCreationSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie (FastHTML's add_toast mechanism) # Toast is injected into response body by FastHTML's toast middleware
# The session cookie contains base64-encoded toast data with "toasts" key assert "Created 2 duck" in resp.text
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
# Base64 decode contains toast message (eyJ0b2FzdHMi... = {"toasts"...)
import base64
# Extract base64 portion from cookie value
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
# FastHTML uses itsdangerous, so format is base64.timestamp.signature
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Created 2 duck" in decoded
class TestCohortCreationValidation: class TestCohortCreationValidation:
@@ -375,7 +363,7 @@ class TestHatchRecordingSuccess:
assert count_at_nursery >= 3 assert count_at_nursery >= 3
def test_hatch_success_returns_toast(self, client, seeded_db, location_strip1_id): def test_hatch_success_returns_toast(self, client, seeded_db, location_strip1_id):
"""Successful hatch recording stores toast in session.""" """Successful hatch recording renders toast in response body."""
resp = client.post( resp = client.post(
"/actions/hatch-recorded", "/actions/hatch-recorded",
data={ data={
@@ -387,16 +375,8 @@ class TestHatchRecordingSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie (FastHTML's add_toast mechanism) # Toast is injected into response body by FastHTML's toast middleware
assert "set-cookie" in resp.headers assert "Recorded 2 hatchling" in resp.text
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
import base64
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Recorded 2 hatchling" in decoded
class TestHatchRecordingValidation: class TestHatchRecordingValidation:
@@ -729,8 +709,7 @@ class TestTagAddSuccess:
assert tag_count >= len(animals_for_tagging) assert tag_count >= len(animals_for_tagging)
def test_tag_add_success_returns_toast(self, client, seeded_db, animals_for_tagging): def test_tag_add_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful tag add stores toast in session.""" """Successful tag add renders toast in response body."""
import base64
import time import time
from animaltrack.selection import compute_roster_hash from animaltrack.selection import compute_roster_hash
@@ -751,14 +730,8 @@ class TestTagAddSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie # Toast is injected into response body by FastHTML's toast middleware
assert "set-cookie" in resp.headers assert "Tagged" in resp.text and "test-tag-toast" in resp.text
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Tagged" in decoded and "test-tag-toast" in decoded
class TestTagAddValidation: class TestTagAddValidation:
@@ -925,8 +898,7 @@ class TestTagEndSuccess:
assert open_after == 0 assert open_after == 0
def test_tag_end_success_returns_toast(self, client, seeded_db, tagged_animals): def test_tag_end_success_returns_toast(self, client, seeded_db, tagged_animals):
"""Successful tag end stores toast in session.""" """Successful tag end renders toast in response body."""
import base64
import time import time
from animaltrack.selection import compute_roster_hash from animaltrack.selection import compute_roster_hash
@@ -947,14 +919,8 @@ class TestTagEndSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie # Toast is injected into response body by FastHTML's toast middleware
assert "set-cookie" in resp.headers assert "Ended tag" in resp.text and "test-end-tag" in resp.text
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Ended tag" in decoded and "test-end-tag" in decoded
class TestTagEndValidation: class TestTagEndValidation:
@@ -1103,8 +1069,7 @@ class TestAttrsSuccess:
assert adult_count == len(animals_for_tagging) assert adult_count == len(animals_for_tagging)
def test_attrs_success_returns_toast(self, client, seeded_db, animals_for_tagging): def test_attrs_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful attrs update stores toast in session.""" """Successful attrs update renders toast in response body."""
import base64
import time import time
from animaltrack.selection import compute_roster_hash from animaltrack.selection import compute_roster_hash
@@ -1125,14 +1090,8 @@ class TestAttrsSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie # Toast is injected into response body by FastHTML's toast middleware
assert "set-cookie" in resp.headers assert "Updated attributes" in resp.text
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Updated attributes" in decoded
class TestAttrsValidation: class TestAttrsValidation:
@@ -1280,8 +1239,7 @@ class TestOutcomeSuccess:
assert harvested_count == len(animals_for_tagging) assert harvested_count == len(animals_for_tagging)
def test_outcome_success_returns_toast(self, client, seeded_db, animals_for_tagging): def test_outcome_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful outcome recording stores toast in session.""" """Successful outcome recording renders toast in response body."""
import base64
import time import time
from animaltrack.selection import compute_roster_hash from animaltrack.selection import compute_roster_hash
@@ -1302,14 +1260,8 @@ class TestOutcomeSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Toast is stored in session cookie # Toast is injected into response body by FastHTML's toast middleware
assert "set-cookie" in resp.headers assert "Recorded sold" in resp.text
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Recorded sold" in decoded
class TestOutcomeValidation: class TestOutcomeValidation:

View File

@@ -198,7 +198,7 @@ class TestMoveAnimalSuccess:
location_strip2_id, location_strip2_id,
ducks_at_strip1, ducks_at_strip1,
): ):
"""Successful move returns session cookie with toast.""" """Successful move renders toast in response body."""
ts_utc = int(time.time() * 1000) ts_utc = int(time.time() * 1000)
filter_str = 'location:"Strip 1"' filter_str = 'location:"Strip 1"'
filter_ast = parse_filter(filter_str) filter_ast = parse_filter(filter_str)
@@ -219,16 +219,8 @@ class TestMoveAnimalSuccess:
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert "set-cookie" in resp.headers # Toast is injected into response body by FastHTML's toast middleware
session_cookie = resp.headers["set-cookie"] assert "Moved 5 animals to Strip 2" in resp.text
assert "session_=" in session_cookie
# Base64 decode contains toast message
import base64
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Moved 5 animals to Strip 2" in decoded
def test_move_success_resets_form( def test_move_success_resets_form(
self, self,