Fix 405 error after event deletion via HX-Push-Url header
All checks were successful
Deploy / deploy (push) Successful in 2m39s
All checks were successful
Deploy / deploy (push) Successful in 2m39s
When HTMX boosted forms submit via POST, the browser URL wasn't being
updated correctly. This caused window.location.reload() after event
deletion to reload the action URL (e.g., /actions/feed-given) instead
of the display URL (e.g., /feed), resulting in a 405 Method Not Allowed.
The fix adds a render_page_post() helper that returns FT components
with an HttpHeader("HX-Push-Url", push_url). This tells HTMX to update
the browser history to the correct URL after successful form submission.
Updated routes:
- /actions/feed-given -> push /feed
- /actions/feed-purchased -> push /feed
- /actions/product-collected -> push /
- /actions/product-sold -> push /
- /actions/animal-move -> push /move
- /actions/animal-cohort -> push /actions/cohort
- /actions/hatch-recorded -> push /actions/hatch
- /actions/animal-tag-add -> push /actions/tag-add
- /actions/animal-tag-end -> push /actions/tag-end
- /actions/animal-attrs -> push /actions/attrs
- /actions/animal-outcome -> push /actions/outcome
- /actions/animal-status-correct -> push /actions/status-correct
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ from animaltrack.selection import compute_roster_hash, parse_filter, resolve_fil
|
||||
from animaltrack.selection.validation import SelectionContext, validate_selection
|
||||
from animaltrack.services.animal import AnimalService, ValidationError
|
||||
from animaltrack.web.auth import UserRole, require_role
|
||||
from animaltrack.web.templates import render_page
|
||||
from animaltrack.web.templates import render_page, render_page_post
|
||||
from animaltrack.web.templates.actions import (
|
||||
attrs_diff_panel,
|
||||
attrs_form,
|
||||
@@ -206,9 +206,11 @@ async def animal_cohort(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
cohort_form(locations, species_list),
|
||||
push_url="/actions/cohort",
|
||||
title="Create Cohort - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -349,9 +351,11 @@ async def hatch_recorded(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
hatch_form(locations, species_list),
|
||||
push_url="/actions/hatch",
|
||||
title="Record Hatch - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -690,9 +694,11 @@ async def animal_tag_add(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
tag_add_form(),
|
||||
push_url="/actions/tag-add",
|
||||
title="Add Tag - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -939,9 +945,11 @@ async def animal_tag_end(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
tag_end_form(),
|
||||
push_url="/actions/tag-end",
|
||||
title="End Tag - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -1175,9 +1183,11 @@ async def animal_attrs(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
attrs_form(),
|
||||
push_url="/actions/attrs",
|
||||
title="Update Attributes - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -1455,10 +1465,11 @@ async def animal_outcome(request: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
product_repo = ProductRepository(db)
|
||||
products = [(p.code, p.name) for p in product_repo.list_all() if p.active]
|
||||
|
||||
return render_page(
|
||||
return render_page_post(
|
||||
request,
|
||||
outcome_form(
|
||||
filter_str="",
|
||||
@@ -1468,6 +1479,7 @@ async def animal_outcome(request: Request, session):
|
||||
resolved_count=0,
|
||||
products=products,
|
||||
),
|
||||
push_url="/actions/outcome",
|
||||
title="Record Outcome - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
@@ -1678,7 +1690,8 @@ async def animal_status_correct(req: Request, session):
|
||||
)
|
||||
|
||||
# Success: re-render fresh form
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
req,
|
||||
status_correct_form(
|
||||
filter_str="",
|
||||
@@ -1687,6 +1700,7 @@ async def animal_status_correct(req: Request, session):
|
||||
ts_utc=int(time.time() * 1000),
|
||||
resolved_count=0,
|
||||
),
|
||||
push_url="/actions/status-correct",
|
||||
title="Correct Status - AnimalTrack",
|
||||
active_nav=None,
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ from animaltrack.repositories.products import ProductRepository
|
||||
from animaltrack.repositories.user_defaults import UserDefaultsRepository
|
||||
from animaltrack.repositories.users import UserRepository
|
||||
from animaltrack.services.products import ProductService, ValidationError
|
||||
from animaltrack.web.templates import render_page
|
||||
from animaltrack.web.templates import render_page, render_page_post
|
||||
from animaltrack.web.templates.eggs import eggs_page
|
||||
|
||||
# 30 days in milliseconds
|
||||
@@ -483,7 +483,8 @@ async def product_collected(request: Request, session):
|
||||
display_data = _get_eggs_display_data(db, locations)
|
||||
|
||||
# Success: re-render form with location sticking, qty cleared
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
eggs_page(
|
||||
locations,
|
||||
@@ -494,6 +495,7 @@ async def product_collected(request: Request, session):
|
||||
sell_action=product_sold,
|
||||
**display_data,
|
||||
),
|
||||
push_url="/",
|
||||
title="Eggs - AnimalTrack",
|
||||
active_nav="eggs",
|
||||
)
|
||||
@@ -652,7 +654,8 @@ async def product_sold(request: Request, session):
|
||||
display_data = _get_eggs_display_data(db, locations)
|
||||
|
||||
# Success: re-render form with product sticking
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
eggs_page(
|
||||
locations,
|
||||
@@ -663,6 +666,7 @@ async def product_sold(request: Request, session):
|
||||
sell_action=product_sold,
|
||||
**display_data,
|
||||
),
|
||||
push_url="/",
|
||||
title="Eggs - AnimalTrack",
|
||||
active_nav="eggs",
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ from animaltrack.repositories.locations import LocationRepository
|
||||
from animaltrack.repositories.user_defaults import UserDefaultsRepository
|
||||
from animaltrack.repositories.users import UserRepository
|
||||
from animaltrack.services.feed import FeedService, ValidationError
|
||||
from animaltrack.web.templates import render_page
|
||||
from animaltrack.web.templates import render_page, render_page_post
|
||||
from animaltrack.web.templates.feed import feed_page
|
||||
|
||||
# 30 days in milliseconds
|
||||
@@ -498,7 +498,8 @@ async def feed_given(request: Request, session):
|
||||
display_data = _get_feed_display_data(db, locations, feed_types)
|
||||
|
||||
# Success: re-render form with location/type sticking, amount reset
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
feed_page(
|
||||
locations,
|
||||
@@ -512,6 +513,7 @@ async def feed_given(request: Request, session):
|
||||
purchase_action=feed_purchased,
|
||||
**display_data,
|
||||
),
|
||||
push_url="/feed",
|
||||
title="Feed - AnimalTrack",
|
||||
active_nav="feed",
|
||||
)
|
||||
@@ -666,7 +668,8 @@ async def feed_purchased(request: Request, session):
|
||||
display_data = _get_feed_display_data(db, locations, feed_types)
|
||||
|
||||
# Success: re-render form with fields cleared
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
feed_page(
|
||||
locations,
|
||||
@@ -676,6 +679,7 @@ async def feed_purchased(request: Request, session):
|
||||
purchase_action=feed_purchased,
|
||||
**display_data,
|
||||
),
|
||||
push_url="/feed",
|
||||
title="Feed - AnimalTrack",
|
||||
active_nav="feed",
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from animaltrack.repositories.locations import LocationRepository
|
||||
from animaltrack.selection import compute_roster_hash, parse_filter, resolve_filter
|
||||
from animaltrack.selection.validation import SelectionContext, validate_selection
|
||||
from animaltrack.services.animal import AnimalService, ValidationError
|
||||
from animaltrack.web.templates import render_page
|
||||
from animaltrack.web.templates import render_page, render_page_post
|
||||
from animaltrack.web.templates.move import diff_panel, move_form
|
||||
|
||||
# Milliseconds per day
|
||||
@@ -396,13 +396,15 @@ async def animal_move(request: Request, session):
|
||||
display_data = _get_move_display_data(db, locations)
|
||||
|
||||
# Success: re-render fresh form (nothing sticks per spec)
|
||||
return render_page(
|
||||
# Use render_page_post to set HX-Push-Url header for correct browser URL
|
||||
return render_page_post(
|
||||
request,
|
||||
move_form(
|
||||
locations,
|
||||
action=animal_move,
|
||||
**display_data,
|
||||
),
|
||||
push_url="/move",
|
||||
title="Move - AnimalTrack",
|
||||
active_nav="move",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: Templates package for AnimalTrack web UI.
|
||||
# ABOUTME: Contains reusable UI components built with FastHTML and MonsterUI.
|
||||
|
||||
from animaltrack.web.templates.base import page, render_page
|
||||
from animaltrack.web.templates.base import page, render_page, render_page_post
|
||||
from animaltrack.web.templates.nav import BottomNav
|
||||
|
||||
__all__ = ["page", "render_page", "BottomNav"]
|
||||
__all__ = ["page", "render_page", "render_page_post", "BottomNav"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: Base HTML template for AnimalTrack pages.
|
||||
# ABOUTME: Provides consistent layout with MonsterUI theme and responsive nav.
|
||||
|
||||
from fasthtml.common import Container, Div, Script, Style, Title
|
||||
from fasthtml.common import Container, Div, HttpHeader, Script, Style, Title
|
||||
from starlette.requests import Request
|
||||
|
||||
from animaltrack.models.reference import UserRole
|
||||
@@ -230,3 +230,26 @@ def render_page(request: Request, content, **page_kwargs):
|
||||
user_role=auth.role if auth else None,
|
||||
**page_kwargs,
|
||||
)
|
||||
|
||||
|
||||
def render_page_post(request: Request, content, push_url: str, **page_kwargs):
|
||||
"""Wrapper for POST responses that sets HX-Push-Url header.
|
||||
|
||||
When HTMX boosted forms submit via POST, the browser URL may not be updated
|
||||
correctly. This wrapper returns the rendered page with an HX-Push-Url header
|
||||
to ensure the browser history shows the correct URL.
|
||||
|
||||
This fixes the issue where window.location.reload() after form submission
|
||||
would reload the wrong URL (the action URL instead of the display URL).
|
||||
|
||||
Args:
|
||||
request: The Starlette request object.
|
||||
content: Page content (FT components).
|
||||
push_url: The URL to push to browser history (e.g., '/feed', '/move').
|
||||
**page_kwargs: Additional arguments passed to page() (title, active_nav).
|
||||
|
||||
Returns:
|
||||
Tuple of (FT components, HttpHeader) that FastHTML processes together.
|
||||
"""
|
||||
page_content = render_page(request, content, **page_kwargs)
|
||||
return (*page_content, HttpHeader("HX-Push-Url", push_url))
|
||||
|
||||
Reference in New Issue
Block a user