Fix MonsterUI Select bug, UI improvements, enable animal rename

- Replace broken MonsterUI LabelSelect with raw HTML Select elements
  (was sending label text instead of value attribute)
- Fix wrong ReproStatus options in promote form (use enum values)
- Add spacing between name and details in animal selection list
- Fix registry filter layout, add Clear button
- Use phonetic ID in animal details panel title
- Change feed price input from cents to euros
- Allow renaming already-identified animals (remove identified check)
- Fix FeedGiven 422 error (same MonsterUI Select bug)

🤖 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-05 15:57:09 +00:00
parent 14bf2fa4ae
commit ad1f91098b
7 changed files with 115 additions and 102 deletions

View File

@@ -408,13 +408,13 @@ def promote_index(request: Request, animal_id: str):
if animal.status != "alive":
return HTMLResponse(content="Only alive animals can be promoted", status_code=400)
if animal.identified:
return HTMLResponse(content="Animal is already identified", status_code=400)
# Title depends on whether animal is already identified (rename vs promote)
title = "Rename Animal - AnimalTrack" if animal.identified else "Promote Animal - AnimalTrack"
return render_page(
request,
promote_form(animal),
title="Promote Animal - AnimalTrack",
title=title,
active_nav=None,
)
@@ -448,9 +448,6 @@ async def animal_promote(request: Request):
if animal.status != "alive":
return _render_promote_error(request, animal, "Only alive animals can be promoted", form)
if animal.identified:
return _render_promote_error(request, animal, "Animal is already identified", form)
# Create payload
try:
payload = AnimalPromotedPayload(

View File

@@ -273,7 +273,7 @@ async def feed_purchased(request: Request, session):
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")
bag_price_euros_str = form.get("bag_price_euros", "0")
vendor = form.get("vendor") or None
notes = form.get("notes") or None
nonce = form.get("nonce")
@@ -329,9 +329,10 @@ async def feed_purchased(request: Request, session):
"Bags count must be at least 1",
)
# Validate bag_price_cents
# Validate bag_price_euros and convert to cents
try:
bag_price_cents = int(bag_price_cents_str)
bag_price_euros = float(bag_price_euros_str)
bag_price_cents = int(round(bag_price_euros * 100))
except ValueError:
return _render_purchase_error(
request,

View File

@@ -4,7 +4,7 @@
from collections.abc import Callable
from typing import Any
from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Span
from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Select, Span
from monsterui.all import (
Alert,
AlertT,
@@ -436,6 +436,11 @@ def promote_form(
"""
display_id = f"{animal.animal_id[:8]}..."
# Title and action text depend on whether animal is already identified
is_rename = animal.identified
form_title = "Rename Animal" if is_rename else "Promote Animal"
action_text = "Renaming" if is_rename else "Promoting"
# Build sex options (optional - can refine current value)
sexes = [
("", "Keep current"),
@@ -446,13 +451,13 @@ def promote_form(
for code, label in sexes:
sex_options.append(Option(label, value=code, selected=code == selected_sex))
# Build repro status options (optional)
# Build repro status options (optional) - must match ReproStatus enum
repro_statuses = [
("", "Unknown"),
("breeding", "Breeding"),
("non_breeding", "Non-Breeding"),
("broody", "Broody"),
("molting", "Molting"),
("", "Keep current"),
("intact", "Intact"),
("wether", "Wether (castrated male)"),
("spayed", "Spayed (female)"),
("unknown", "Unknown"),
]
repro_status_options = []
for code, label in repro_statuses:
@@ -466,36 +471,34 @@ def promote_form(
error_component = Alert(error, cls=AlertT.warning)
return Form(
H2("Promote Animal", cls="text-xl font-bold mb-4"),
H2(form_title, cls="text-xl font-bold mb-4"),
# Animal info
Div(
P(f"Promoting: {display_id}", cls="text-sm text-stone-400"),
P(f"{action_text}: {display_id}", cls="text-sm text-stone-400"),
P(f"Species: {animal.species_code}, Sex: {animal.sex}", cls="text-sm text-stone-400"),
cls="p-3 bg-slate-800 rounded-md mb-4",
),
# Error message if present
error_component,
# Nickname input (optional)
# Nickname input - label changes for rename vs promote
LabelInput(
"Nickname (optional)",
"New Name" if is_rename else "Nickname (optional)",
id="nickname",
name="nickname",
value=nickname_value,
placeholder="Give this animal a name",
value=nickname_value or (animal.nickname or ""),
placeholder="Enter a name for this animal",
),
# Sex refinement dropdown (optional)
LabelSelect(
*sex_options,
label="Refine Sex (optional)",
id="sex",
name="sex",
# Sex refinement dropdown (optional) - using raw Select to fix value handling
Div(
FormLabel("Refine Sex (optional)", _for="sex"),
Select(*sex_options, name="sex", id="sex", cls="uk-select"),
cls="space-y-2",
),
# Repro status dropdown (optional)
LabelSelect(
*repro_status_options,
label="Reproductive Status",
id="repro_status",
name="repro_status",
# Repro status dropdown (optional) - using raw Select to fix value handling
Div(
FormLabel("Reproductive Status", _for="repro_status"),
Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"),
cls="space-y-2",
),
# Distinguishing traits (optional)
LabelTextArea(
@@ -515,8 +518,12 @@ def promote_form(
# Hidden fields
Hidden(name="animal_id", value=animal.animal_id),
Hidden(name="nonce", value=str(ULID())),
# Submit button
Button("Promote to Identified", type="submit", cls=ButtonT.primary),
# Submit button - text changes for rename vs promote
Button(
"Save Changes" if is_rename else "Promote to Identified",
type="submit",
cls=ButtonT.primary,
),
# Form submission via standard action/method (hx-boost handles AJAX)
action=action,
method="post",
@@ -1071,24 +1078,21 @@ def attrs_form(
),
# Selection container - updated via HTMX when filter changes
selection_container,
# Attribute dropdowns
LabelSelect(
*sex_options,
label="Sex",
id="sex",
name="sex",
# Attribute dropdowns - using raw Select to fix value handling
Div(
FormLabel("Sex", _for="sex"),
Select(*sex_options, name="sex", id="sex", cls="uk-select"),
cls="space-y-2",
),
LabelSelect(
*life_stage_options,
label="Life Stage",
id="life_stage",
name="life_stage",
Div(
FormLabel("Life Stage", _for="life_stage"),
Select(*life_stage_options, name="life_stage", id="life_stage", cls="uk-select"),
cls="space-y-2",
),
LabelSelect(
*repro_status_options,
label="Reproductive Status",
id="repro_status",
name="repro_status",
Div(
FormLabel("Reproductive Status", _for="repro_status"),
Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"),
cls="space-y-2",
),
# Optional notes
LabelTextArea(

View File

@@ -7,6 +7,7 @@ from typing import Any
from fasthtml.common import H2, H3, A, Div, Li, P, Span, Ul
from monsterui.all import Button, ButtonT, Card, Grid
from animaltrack.id_gen import format_animal_id
from animaltrack.repositories.animal_timeline import (
AnimalDetail,
MergeInfo,
@@ -61,7 +62,7 @@ def back_to_registry_link() -> Div:
def animal_header_card(animal: AnimalDetail, merge_info: MergeInfo | None) -> Card:
"""Header card with animal summary."""
display_name = animal.nickname or f"{animal.animal_id[:8]}..."
display_name = format_animal_id(animal.animal_id, animal.nickname)
status_badge = status_badge_component(animal.status)
tags_display = (
@@ -160,10 +161,11 @@ def quick_actions_card(animal: AnimalDetail) -> Card:
href=f"/actions/tag-add?filter=animal_id:{animal.animal_id}",
)
)
if not animal.identified:
# Show "Promote" for unidentified animals, "Rename" for identified ones
promote_label = "Rename" if animal.identified else "Promote"
actions.append(
A(
Button("Promote", cls=ButtonT.default + " w-full"),
Button(promote_label, cls=ButtonT.default + " w-full"),
href=f"/actions/promote/{animal.animal_id}",
)
)

View File

@@ -47,9 +47,9 @@ def animal_checkbox_list(
cls="uk-checkbox mr-2",
hx_on_change="updateSelectionCount()",
),
Span(display_name, cls="text-stone-200"),
Span(display_name, cls="text-stone-200 mr-1"),
Span(
f" ({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})",
f"({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})",
cls="text-stone-500 text-sm",
),
cls="flex items-center py-1 hover:bg-stone-800/30 px-2 rounded cursor-pointer",

View File

@@ -4,12 +4,12 @@
from collections.abc import Callable
from typing import Any
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Select, Ul
from monsterui.all import (
Button,
ButtonT,
FormLabel,
LabelInput,
LabelSelect,
LabelTextArea,
TabContainer,
)
@@ -170,19 +170,17 @@ def give_feed_form(
H2("Give Feed", cls="text-xl font-bold mb-4"),
error_component,
warning_component,
# Location dropdown
LabelSelect(
*location_options,
label="Location",
id="location_id",
name="location_id",
# Location dropdown - using raw Select to fix value handling
Div(
FormLabel("Location", _for="location_id"),
Select(*location_options, name="location_id", id="location_id", cls="uk-select"),
cls="space-y-2",
),
# Feed type dropdown
LabelSelect(
*feed_type_options,
label="Feed Type",
id="feed_type_code",
name="feed_type_code",
# Feed type dropdown - using raw Select to fix value handling
Div(
FormLabel("Feed Type", _for="feed_type_code"),
Select(*feed_type_options, name="feed_type_code", id="feed_type_code", cls="uk-select"),
cls="space-y-2",
),
# Amount input
LabelInput(
@@ -247,12 +245,16 @@ def purchase_feed_form(
return Form(
H2("Purchase Feed", cls="text-xl font-bold mb-4"),
error_component,
# Feed type dropdown
LabelSelect(
# Feed type dropdown - using raw Select to fix value handling
Div(
FormLabel("Feed Type", _for="purchase_feed_type_code"),
Select(
*feed_type_options,
label="Feed Type",
id="purchase_feed_type_code",
name="feed_type_code",
id="purchase_feed_type_code",
cls="uk-select",
),
cls="space-y-2",
),
# Bag size
LabelInput(
@@ -276,15 +278,15 @@ def purchase_feed_form(
value="1",
required=True,
),
# Price per bag (cents)
# Price per bag (euros)
LabelInput(
"Price per Bag (cents)",
id="bag_price_cents",
name="bag_price_cents",
"Price per Bag ()",
id="bag_price_euros",
name="bag_price_euros",
type="number",
min="0",
step="1",
placeholder="e.g., 2400 for 24.00",
step="0.01",
placeholder="e.g., 24.00",
required=True,
),
# Optional vendor

View File

@@ -5,8 +5,8 @@ from datetime import UTC, datetime
from typing import Any
from urllib.parse import urlencode
from fasthtml.common import H2, A, Div, Form, P, Span, Table, Tbody, Td, Th, Thead, Tr
from monsterui.all import Button, ButtonT, Grid, LabelInput
from fasthtml.common import H2, A, Div, Form, Input, P, Span, Table, Tbody, Td, Th, Thead, Tr
from monsterui.all import Button, ButtonT, FormLabel, Grid
from animaltrack.id_gen import format_animal_id
from animaltrack.models.reference import Location, Species
@@ -75,21 +75,28 @@ def registry_header(filter_str: str, total_count: int) -> Div:
),
# Filter form - full width, prominent
Form(
# Label above the input row
FormLabel("Filter", _for="filter", cls="mb-2 block"),
Div(
# Filter input - wider with flex-grow
Div(
LabelInput(
"Filter",
# Filter input - takes most of the width
Input(
id="filter",
name="filter",
value=filter_str,
placeholder='species:duck status:alive location:"Strip 1"',
cls="uk-input flex-1",
),
cls="flex-1 min-w-0", # min-w-0 prevents flex overflow
),
# Apply button - fixed size
Button("Apply", type="submit", cls=f"{ButtonT.primary} shrink-0"),
cls="flex gap-3 items-end",
# Apply button
Button("Apply", type="submit", cls=f"{ButtonT.primary} px-4"),
# Clear button (only shown if filter is active)
A(
"Clear",
href="/registry",
cls="px-3 py-2 text-stone-400 hover:text-stone-200",
)
if filter_str
else None,
cls="flex gap-2 items-center",
),
action="/registry",
method="get",