refactor: move route handlers to module level for idiomatic FastHTML

- Routes are now at module level, accessible for import by templates
- Templates accept action parameter (route function or URL string)
- Routes pass themselves to templates for type-safe form actions
- Changes DB access pattern from app.state.db to request.app.state.db
- Registration uses rt(...)(func) pattern instead of @rt decorator

This enables the idiomatic FastHTML pattern where forms can use
action=route_function instead of action="/path/string", providing
type safety and refactoring support.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 11:11:08 +00:00
parent 600d5003ed
commit b1bfdfb05c
4 changed files with 454 additions and 421 deletions

View File

@@ -49,6 +49,111 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st
return [row[0] for row in rows]
def egg_index(request: Request):
"""GET / - Egg Quick Capture form."""
db = request.app.state.db
locations = LocationRepository(db).list_active()
# Check for pre-selected location from query params
selected_location_id = request.query_params.get("location_id")
return page(
egg_form(locations, selected_location_id=selected_location_id, action=product_collected),
title="Egg - AnimalTrack",
active_nav="egg",
)
async def product_collected(request: Request):
"""POST /actions/product-collected - Record egg collection."""
db = request.app.state.db
form = await request.form()
# Extract form data
location_id = form.get("location_id", "")
quantity_str = form.get("quantity", "0")
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get locations for potential re-render
locations = LocationRepository(db).list_active()
# Validate location_id
if not location_id:
return _render_error_form(locations, None, "Please select a location")
# Validate quantity
try:
quantity = int(quantity_str)
except ValueError:
return _render_error_form(locations, location_id, "Quantity must be a number")
if quantity < 1:
return _render_error_form(locations, location_id, "Quantity must be at least 1")
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Resolve ducks at location
resolved_ids = resolve_ducks_at_location(db, location_id, ts_utc)
if not resolved_ids:
return _render_error_form(locations, location_id, "No ducks at this location")
# Create product service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(AnimalRegistryProjection(db))
registry.register(EventAnimalsProjection(db))
registry.register(IntervalProjection(db))
registry.register(ProductsProjection(db))
product_service = ProductService(db, event_store, registry)
# Create payload
payload = ProductCollectedPayload(
location_id=location_id,
product_code="egg.duck",
quantity=quantity,
resolved_ids=resolved_ids,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Collect product
try:
product_service.collect_product(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/product-collected",
)
except ValidationError as e:
return _render_error_form(locations, location_id, str(e))
# Success: re-render form with location sticking, qty cleared
response = HTMLResponse(
content=str(
page(
egg_form(locations, selected_location_id=location_id, action=product_collected),
title="Egg - AnimalTrack",
active_nav="egg",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{"showToast": {"message": f"Recorded {quantity} eggs", "type": "success"}}
)
return response
def register_egg_routes(rt, app):
"""Register egg capture routes.
@@ -56,111 +161,8 @@ def register_egg_routes(rt, app):
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/")
def index(request: Request):
"""GET / - Egg Quick Capture form."""
db = app.state.db
locations = LocationRepository(db).list_active()
# Check for pre-selected location from query params
selected_location_id = request.query_params.get("location_id")
return page(
egg_form(locations, selected_location_id=selected_location_id),
title="Egg - AnimalTrack",
active_nav="egg",
)
@rt("/actions/product-collected", methods=["POST"])
async def product_collected(request: Request):
"""POST /actions/product-collected - Record egg collection."""
db = app.state.db
form = await request.form()
# Extract form data
location_id = form.get("location_id", "")
quantity_str = form.get("quantity", "0")
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get locations for potential re-render
locations = LocationRepository(db).list_active()
# Validate location_id
if not location_id:
return _render_error_form(locations, None, "Please select a location")
# Validate quantity
try:
quantity = int(quantity_str)
except ValueError:
return _render_error_form(locations, location_id, "Quantity must be a number")
if quantity < 1:
return _render_error_form(locations, location_id, "Quantity must be at least 1")
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Resolve ducks at location
resolved_ids = resolve_ducks_at_location(db, location_id, ts_utc)
if not resolved_ids:
return _render_error_form(locations, location_id, "No ducks at this location")
# Create product service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(AnimalRegistryProjection(db))
registry.register(EventAnimalsProjection(db))
registry.register(IntervalProjection(db))
registry.register(ProductsProjection(db))
product_service = ProductService(db, event_store, registry)
# Create payload
payload = ProductCollectedPayload(
location_id=location_id,
product_code="egg.duck",
quantity=quantity,
resolved_ids=resolved_ids,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Collect product
try:
product_service.collect_product(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/product-collected",
)
except ValidationError as e:
return _render_error_form(locations, location_id, str(e))
# Success: re-render form with location sticking, qty cleared
response = HTMLResponse(
content=str(
page(
egg_form(locations, selected_location_id=location_id),
title="Egg - AnimalTrack",
active_nav="egg",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{"showToast": {"message": f"Recorded {quantity} eggs", "type": "success"}}
)
return response
rt("/")(egg_index)
rt("/actions/product-collected", methods=["POST"])(product_collected)
def _render_error_form(locations, selected_location_id, error_message):
@@ -181,6 +183,7 @@ def _render_error_form(locations, selected_location_id, error_message):
locations,
selected_location_id=selected_location_id,
error=error_message,
action=product_collected,
),
title="Egg - AnimalTrack",
active_nav="egg",

View File

@@ -38,6 +38,324 @@ def get_feed_balance(db: Any, feed_type_code: str) -> int | None:
return row[0] if row else None
def feed_index(request: Request):
"""GET /feed - Feed Quick Capture page."""
db = request.app.state.db
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Check for active tab from query params
active_tab = request.query_params.get("tab", "give")
if active_tab not in ("give", "purchase"):
active_tab = "give"
return page(
feed_page(
locations,
feed_types,
active_tab=active_tab,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
async def feed_given(request: Request):
"""POST /actions/feed-given - Record feed given."""
db = request.app.state.db
form = await request.form()
# Extract form data
location_id = form.get("location_id", "")
feed_type_code = form.get("feed_type_code", "")
amount_kg_str = form.get("amount_kg", "0")
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get data for potential re-render
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Find default amount from feed type
default_amount_kg = 20
for ft in feed_types:
if ft.code == feed_type_code:
default_amount_kg = ft.default_bag_size_kg
break
# Validate location_id
if not location_id:
return _render_give_error(
locations,
feed_types,
"Please select a location",
selected_location_id=None,
selected_feed_type_code=feed_type_code or None,
)
# Validate feed_type_code
if not feed_type_code:
return _render_give_error(
locations,
feed_types,
"Please select a feed type",
selected_location_id=location_id,
selected_feed_type_code=None,
)
# Validate amount_kg
try:
amount_kg = int(amount_kg_str)
except ValueError:
return _render_give_error(
locations,
feed_types,
"Amount must be a number",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
if amount_kg < 1:
return _render_give_error(
locations,
feed_types,
"Amount must be at least 1 kg",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Create feed service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(FeedInventoryProjection(db))
feed_service = FeedService(db, event_store, registry)
# Create payload
payload = FeedGivenPayload(
location_id=location_id,
feed_type_code=feed_type_code,
amount_kg=amount_kg,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Give feed
try:
feed_service.give_feed(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/feed-given",
)
except ValidationError as e:
return _render_give_error(
locations,
feed_types,
str(e),
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
# Check for negative balance warning
balance = get_feed_balance(db, feed_type_code)
balance_warning = None
if balance is not None and balance < 0:
balance_warning = f"Warning: Inventory balance is now negative ({balance} kg)"
# Success: re-render form with location/type sticking, amount reset
response = HTMLResponse(
content=str(
page(
feed_page(
locations,
feed_types,
active_tab="give",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
default_amount_kg=default_amount_kg,
balance_warning=balance_warning,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{
"showToast": {
"message": f"Recorded {amount_kg}kg {feed_type_code}",
"type": "success",
}
}
)
return response
async def feed_purchased(request: Request):
"""POST /actions/feed-purchased - Record feed purchase."""
db = request.app.state.db
form = await request.form()
# Extract form data
feed_type_code = form.get("feed_type_code", "")
bag_size_kg_str = form.get("bag_size_kg", "0")
bags_count_str = form.get("bags_count", "0")
bag_price_cents_str = form.get("bag_price_cents", "0")
vendor = form.get("vendor") or None
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get data for potential re-render
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Validate feed_type_code
if not feed_type_code:
return _render_purchase_error(
locations,
feed_types,
"Please select a feed type",
)
# Validate bag_size_kg
try:
bag_size_kg = int(bag_size_kg_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Bag size must be a number",
)
if bag_size_kg < 1:
return _render_purchase_error(
locations,
feed_types,
"Bag size must be at least 1 kg",
)
# Validate bags_count
try:
bags_count = int(bags_count_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Bags count must be a number",
)
if bags_count < 1:
return _render_purchase_error(
locations,
feed_types,
"Bags count must be at least 1",
)
# Validate bag_price_cents
try:
bag_price_cents = int(bag_price_cents_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Price must be a number",
)
if bag_price_cents < 0:
return _render_purchase_error(
locations,
feed_types,
"Price cannot be negative",
)
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Create feed service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(FeedInventoryProjection(db))
feed_service = FeedService(db, event_store, registry)
# Create payload
payload = FeedPurchasedPayload(
feed_type_code=feed_type_code,
bag_size_kg=bag_size_kg,
bags_count=bags_count,
bag_price_cents=bag_price_cents,
vendor=vendor,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Purchase feed
try:
feed_service.purchase_feed(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/feed-purchased",
)
except ValidationError as e:
return _render_purchase_error(
locations,
feed_types,
str(e),
)
# Calculate total for toast
total_kg = bag_size_kg * bags_count
# Success: re-render form with fields cleared
response = HTMLResponse(
content=str(
page(
feed_page(
locations,
feed_types,
active_tab="purchase",
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{
"showToast": {
"message": f"Purchased {total_kg}kg {feed_type_code}",
"type": "success",
}
}
)
return response
def register_feed_routes(rt, app):
"""Register feed capture routes.
@@ -45,318 +363,9 @@ def register_feed_routes(rt, app):
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/feed")
def feed_index(request: Request):
"""GET /feed - Feed Quick Capture page."""
db = app.state.db
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Check for active tab from query params
active_tab = request.query_params.get("tab", "give")
if active_tab not in ("give", "purchase"):
active_tab = "give"
return page(
feed_page(
locations,
feed_types,
active_tab=active_tab,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
@rt("/actions/feed-given", methods=["POST"])
async def feed_given(request: Request):
"""POST /actions/feed-given - Record feed given."""
db = app.state.db
form = await request.form()
# Extract form data
location_id = form.get("location_id", "")
feed_type_code = form.get("feed_type_code", "")
amount_kg_str = form.get("amount_kg", "0")
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get data for potential re-render
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Find default amount from feed type
default_amount_kg = 20
for ft in feed_types:
if ft.code == feed_type_code:
default_amount_kg = ft.default_bag_size_kg
break
# Validate location_id
if not location_id:
return _render_give_error(
locations,
feed_types,
"Please select a location",
selected_location_id=None,
selected_feed_type_code=feed_type_code or None,
)
# Validate feed_type_code
if not feed_type_code:
return _render_give_error(
locations,
feed_types,
"Please select a feed type",
selected_location_id=location_id,
selected_feed_type_code=None,
)
# Validate amount_kg
try:
amount_kg = int(amount_kg_str)
except ValueError:
return _render_give_error(
locations,
feed_types,
"Amount must be a number",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
if amount_kg < 1:
return _render_give_error(
locations,
feed_types,
"Amount must be at least 1 kg",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Create feed service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(FeedInventoryProjection(db))
feed_service = FeedService(db, event_store, registry)
# Create payload
payload = FeedGivenPayload(
location_id=location_id,
feed_type_code=feed_type_code,
amount_kg=amount_kg,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Give feed
try:
feed_service.give_feed(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/feed-given",
)
except ValidationError as e:
return _render_give_error(
locations,
feed_types,
str(e),
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
)
# Check for negative balance warning
balance = get_feed_balance(db, feed_type_code)
balance_warning = None
if balance is not None and balance < 0:
balance_warning = f"Warning: Inventory balance is now negative ({balance} kg)"
# Success: re-render form with location/type sticking, amount reset
response = HTMLResponse(
content=str(
page(
feed_page(
locations,
feed_types,
active_tab="give",
selected_location_id=location_id,
selected_feed_type_code=feed_type_code,
default_amount_kg=default_amount_kg,
balance_warning=balance_warning,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{
"showToast": {
"message": f"Recorded {amount_kg}kg {feed_type_code}",
"type": "success",
}
}
)
return response
@rt("/actions/feed-purchased", methods=["POST"])
async def feed_purchased(request: Request):
"""POST /actions/feed-purchased - Record feed purchase."""
db = app.state.db
form = await request.form()
# Extract form data
feed_type_code = form.get("feed_type_code", "")
bag_size_kg_str = form.get("bag_size_kg", "0")
bags_count_str = form.get("bags_count", "0")
bag_price_cents_str = form.get("bag_price_cents", "0")
vendor = form.get("vendor") or None
notes = form.get("notes") or None
nonce = form.get("nonce")
# Get data for potential re-render
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
# Validate feed_type_code
if not feed_type_code:
return _render_purchase_error(
locations,
feed_types,
"Please select a feed type",
)
# Validate bag_size_kg
try:
bag_size_kg = int(bag_size_kg_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Bag size must be a number",
)
if bag_size_kg < 1:
return _render_purchase_error(
locations,
feed_types,
"Bag size must be at least 1 kg",
)
# Validate bags_count
try:
bags_count = int(bags_count_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Bags count must be a number",
)
if bags_count < 1:
return _render_purchase_error(
locations,
feed_types,
"Bags count must be at least 1",
)
# Validate bag_price_cents
try:
bag_price_cents = int(bag_price_cents_str)
except ValueError:
return _render_purchase_error(
locations,
feed_types,
"Price must be a number",
)
if bag_price_cents < 0:
return _render_purchase_error(
locations,
feed_types,
"Price cannot be negative",
)
# Get current timestamp
ts_utc = int(time.time() * 1000)
# Create feed service
event_store = EventStore(db)
registry = ProjectionRegistry()
registry.register(FeedInventoryProjection(db))
feed_service = FeedService(db, event_store, registry)
# Create payload
payload = FeedPurchasedPayload(
feed_type_code=feed_type_code,
bag_size_kg=bag_size_kg,
bags_count=bags_count,
bag_price_cents=bag_price_cents,
vendor=vendor,
notes=notes,
)
# Get actor from auth
auth = request.scope.get("auth")
actor = auth.username if auth else "unknown"
# Purchase feed
try:
feed_service.purchase_feed(
payload=payload,
ts_utc=ts_utc,
actor=actor,
nonce=nonce,
route="/actions/feed-purchased",
)
except ValidationError as e:
return _render_purchase_error(
locations,
feed_types,
str(e),
)
# Calculate total for toast
total_kg = bag_size_kg * bags_count
# Success: re-render form with fields cleared
response = HTMLResponse(
content=str(
page(
feed_page(
locations,
feed_types,
active_tab="purchase",
),
title="Feed - AnimalTrack",
active_nav="feed",
)
),
)
# Add toast trigger header
response.headers["HX-Trigger"] = json.dumps(
{
"showToast": {
"message": f"Purchased {total_kg}kg {feed_type_code}",
"type": "success",
}
}
)
return response
rt("/feed")(feed_index)
rt("/actions/feed-given", methods=["POST"])(feed_given)
rt("/actions/feed-purchased", methods=["POST"])(feed_purchased)
def _render_give_error(
@@ -388,6 +397,8 @@ def _render_give_error(
selected_location_id=selected_location_id,
selected_feed_type_code=selected_feed_type_code,
give_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
@@ -416,6 +427,8 @@ def _render_purchase_error(locations, feed_types, error_message):
feed_types,
active_tab="purchase",
purchase_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Egg Quick Capture form.
# ABOUTME: Provides form components for recording egg collections.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H2, Form, Hidden, Option
from monsterui.all import Button, ButtonT, LabelInput, LabelSelect, LabelTextArea
from ulid import ULID
@@ -12,6 +15,7 @@ def egg_form(
locations: list[Location],
selected_location_id: str | None = None,
error: str | None = None,
action: Callable[..., Any] | str = "/actions/product-collected",
) -> Form:
"""Create the Egg Quick Capture form.
@@ -19,6 +23,7 @@ def egg_form(
locations: List of active locations for the dropdown.
selected_location_id: Pre-selected location ID (sticks after submission).
error: Optional error message to display.
action: Route function or URL string for form submission.
Returns:
Form component for egg collection.
@@ -82,7 +87,7 @@ def egg_form(
# Submit button
Button("Record Eggs", type="submit", cls=ButtonT.primary),
# Form submission via standard action/method (hx-boost handles AJAX)
action="/actions/product-collected",
action=action,
method="post",
cls="space-y-4",
)

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Feed Quick Capture forms.
# ABOUTME: Provides form components for recording feed given and purchases.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul
from monsterui.all import (
Button,
@@ -25,6 +28,8 @@ def feed_page(
give_error: str | None = None,
purchase_error: str | None = None,
balance_warning: str | None = None,
give_action: Callable[..., Any] | str = "/actions/feed-given",
purchase_action: Callable[..., Any] | str = "/actions/feed-purchased",
):
"""Create the Feed Quick Capture page with tabbed forms.
@@ -38,6 +43,8 @@ def feed_page(
give_error: Error message for give form.
purchase_error: Error message for purchase form.
balance_warning: Warning about negative inventory balance.
give_action: Route function or URL for give feed form.
purchase_action: Route function or URL for purchase feed form.
Returns:
Page content with tabbed forms.
@@ -76,11 +83,12 @@ def feed_page(
default_amount_kg=default_amount_kg,
error=give_error,
balance_warning=balance_warning,
action=give_action,
),
cls="uk-active" if give_active else "",
),
Li(
purchase_feed_form(feed_types, error=purchase_error),
purchase_feed_form(feed_types, error=purchase_error, action=purchase_action),
cls="" if give_active else "uk-active",
),
),
@@ -96,6 +104,7 @@ def give_feed_form(
default_amount_kg: int | None = None,
error: str | None = None,
balance_warning: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-given",
) -> Form:
"""Create the Give Feed form.
@@ -107,6 +116,7 @@ def give_feed_form(
default_amount_kg: Default value for amount field.
error: Error message to display.
balance_warning: Warning about negative balance.
action: Route function or URL for form submission.
Returns:
Form component for giving feed.
@@ -196,7 +206,7 @@ def give_feed_form(
Hidden(name="nonce", value=str(ULID())),
# Submit button
Button("Record Feed Given", type="submit", cls=ButtonT.primary),
action="/actions/feed-given",
action=action,
method="post",
cls="space-y-4",
)
@@ -205,12 +215,14 @@ def give_feed_form(
def purchase_feed_form(
feed_types: list[FeedType],
error: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-purchased",
) -> Form:
"""Create the Purchase Feed form.
Args:
feed_types: List of active feed types.
error: Error message to display.
action: Route function or URL for form submission.
Returns:
Form component for purchasing feed.
@@ -290,7 +302,7 @@ def purchase_feed_form(
Hidden(name="nonce", value=str(ULID())),
# Submit button
Button("Record Purchase", type="submit", cls=ButtonT.primary),
action="/actions/feed-purchased",
action=action,
method="post",
cls="space-y-4",
)