The event_datetime_field JavaScript used querySelector to find the
ts_utc hidden input by name, which breaks when multiple forms have
ts_utc fields (like feed give and purchase forms). Now each hidden
field gets a unique ID based on the field_id parameter, and the
JavaScript uses getElementById for correct scoping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The 422 error handlers were using str() to convert FT objects to HTML,
which produces Python repr output instead of HTML. Changed to use
to_xml() like other routes (eggs.py, products.py) do.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The registry selection feature builds filters like animal_id:X|Y|Z
but the parser didn't recognize animal_id as a valid field.
- Add animal_id to VALID_FIELDS in parser.py
- Add animal_id handler in resolver.py _build_filter_clause
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Registry improvements:
- Add checkbox column for selecting animals in the table
- Add selection toolbar with count display
- Add Actions dropdown (Move, Add Tag, Update Attributes, Record Outcome)
- Selection persists across infinite scroll via JavaScript
- Navigate to action page with filter=animal_id:X|Y|Z for selected animals
Event detail improvements:
- Show more animal details: sex (M/F/?), life stage, location name
- Add "Show all X animals" button when >20 animals affected
- HTMX endpoint to load full list on demand
- Separate affected_animals_list component for HTMX swaps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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>
- Fix CSRF token handling for production: generate tokens with HMAC,
set cookie via afterware, inject into HTMX requests via JS
- Improve registry page: filter at top with better proportions,
compact horizontal pill layout for facets
- Add phonetic ID encoding (e.g., "tobi-kafu-meli") for animal display
instead of truncated ULIDs
- Remove "subadult" life stage (migration converts to juvenile)
- Change "Death (natural)" outcome label to just "Death"
- Show sex/life stage in animal picker alongside species/location
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update docker image path and workflow reference to use alo org.
Fix var directory permissions in docker build.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify workflow to use reusable workflow:
uses: ppetru/alo-cluster/.gitea/workflows/deploy-nomad.yaml@master
All build/push/deploy logic is now centralized in alo-cluster.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Evaluate alone doesn't create deployments. We need to resubmit
the job with a changed meta.uuid to force Nomad to deploy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use skopeo and jq directly (already in nix-runner image)
- Redirect evaluate response to /dev/null
- Echo responses for debugging
- Handle case where no deployment exists
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
runAsRoot requires KVM which isn't available in CI containers.
extraCommands achieves the same result without virtualization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TRUSTED_PROXY_IPS now accepts CIDR notation (e.g., 192.168.1.0/24)
in addition to exact IP addresses. Supports both IPv4 and IPv6.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix event detail page for direct navigation by using FastHTML's
idiomatic htmx parameter instead of manual header check
- Add custom toaster.py with HTML support using NotStr to render
clickable links in toast messages
- Add hx_preserve to toast container to survive HTMX body swaps
- Add is_deleted column to event_log_by_location table
- Update event_log projection revert() to set is_deleted flag
instead of deleting rows
- Add strikethrough styling for deleted events in event log
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
FastHTML omits empty string attributes (value=""), causing browsers to
submit the option's text content "Same as hatch location" instead of an
empty value. This resulted in a ULID validation error.
Use "__none__" as a sentinel value that the server converts to None.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
Two bugs fixed in animal selection with checkboxes:
1. Confirmation dialog showed wrong count (e.g., "35 animals" instead of
"2 animals" when only 2 were selected). Fixed by using valid_selected
count in diff.server_count instead of full filter resolution count.
2. Spurious "Selection Changed" dialogs due to race condition in async
hash computation. Fixed by removing client-side hash computation
entirely - it was unnecessary since the server validates selected_ids
directly against the filter resolution.
Changes:
- validation.py: Remove hash comparison in _validate_subset(), validate
IDs directly, fix server_count in diff
- animal_select.py: Remove computeSelectionHash(), hidden roster_hash
field, and related async fetch code
- test_selection_validation.py: Add tests for subset mode validation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 409 to HTMX responseHandling config so selection conflict
dialogs are displayed instead of silently ignored
- Fix event type dropdown using value="" which caused browsers to
send display text "All types" instead of empty string
- Use value="all" for "All types" option (matching location selector)
- Handle event_type="all" in route as no filter
- Remove dead code (location_name lookup duplicated in template)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add tooltips with location ID on hover for location names in event detail
- Make location names clickable links to /locations/{id} detail page
- Create location detail page showing location info, live animal count,
and recent events at that location
- Add public GET /locations/{id} route (existing admin routes unchanged)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The CLI command only needs DB_PATH for database operations, not web
settings like csrf_secret. Read DB_PATH directly from environment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 'rebuild-projections' CLI command that truncates all projection
tables and replays non-tombstoned events to rebuild state
- Fix event delete route to register all projections before calling
delete_event, ensuring projections are properly reverted
- Add comprehensive tests for both rebuild CLI and delete with projections
The rebuild-projections command is useful for recovering from corrupted
projection state, while the delete fix ensures future deletes properly
revert animal status (e.g., sold -> alive).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
app.state.registry was never set - create ProjectionRegistry()
locally like all other routes do.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix UserRole.admin to UserRole.ADMIN in events.py delete route
- Add content-type check before parsing JSON in event delete handler
- Add error handling and content-type check in animal selection hash computation
- Audited codebase: no other enum case issues found
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix event log white background to use dark theme (bg-[#141413])
- Fix UserRole.admin typo to UserRole.ADMIN in event_detail.py
- Add global exception handler that logs errors and shows toast
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix checkbox selection not working: remove duplicate subset_mode hidden
fields from 5 form templates and add resolved_ids to checkbox component
- Add all-events view to event log (shows events without location like
AnimalOutcome, FeedPurchased, ProductSold)
- Add event type filter dropdown alongside location filter
- Make event log items clickable to open event detail slide-over
- Add event delete UI with confirmation dialog (admin only)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When users type a filter, HTMX now fetches the matching animals and
displays a checkbox list for subset selection. Changes:
- Add hx_get="/api/selection-preview" to filter inputs in all forms
- Wrap selection component in #selection-container for HTMX targeting
- Add subset_mode hidden field to checkbox list component
- Handle single-animal case with simple count display (no checkboxes)
Forms updated: outcome, tag-add, tag-end, attrs, move
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Three major features implemented:
1. Event Detail Slide-Over Panel
- Click timeline events to view details in slide-over
- New /events/{event_id} route and event_detail.py template
- Type-specific payload rendering for all event types
2. Toast System Refactor
- Switch from custom addEventListener to FastHTML's add_toast()
- Replace HX-Trigger headers with session-based toasts
- Add event links in toast messages
- Replace addEventListener with hx_on_* in templates
3. Checkbox Selection for Animal Subsets
- New animal_select.py component with checkbox list
- New /api/compute-hash and /api/selection-preview endpoints
- Add subset_mode support to SelectionContext validation
- Update 5 forms: outcome, move, tag-add, tag-end, attrs
- Users can select specific animals from filtered results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Python's sqlite3.executescript() has a bug where trailing newlines after
the final semicolon create empty statements. When APSW's log_sqlite() is
enabled (via apswutils, imported by fastmigrate), these cause visible
"API called with NULL prepared statement" errors during interpreter shutdown.
- Strip trailing newlines from all 9 existing migration files
- Update migration template to end with semicolon, no trailing newline
- Document the requirement in CLAUDE.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added optional notes field to forms that support it in their payloads
but were missing the UI input:
- cohort_form (Create Animal Cohort)
- hatch_form (Record Hatch)
- promote_form (Promote Animal)
- tag_add_form (Add Tag)
- tag_end_form (End Tag)
- attrs_form (Update Attributes)
- move_form (Move Animals)
Uses consistent LabelTextArea pattern matching existing forms like
outcome_form and status_correct_form.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create render_page() wrapper that auto-extracts auth from request
for page() calls. This eliminates manual username/user_role passing
and ensures consistent auth handling across all routes.
Updated all route files to use render_page():
- actions.py, eggs.py, feed.py, move.py, products.py
- animals.py, events.py, locations.py, registry.py
This fixes "Guest" username display on forms by ensuring auth
is always extracted from request.scope.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The event_datetime_field component now accepts initial_value and
initial_ts parameters. When re-rendering a form after validation
error, the datetime picker value is preserved and shown expanded.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The validation now checks that select values are in the allowed set,
not just non-empty. This catches cases where browsers send placeholder
text (e.g., "Select location...") instead of empty string for disabled
options.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Configure HTMX to swap 422 responses so validation errors display
- Add missing name="notes" to egg harvest and product sold forms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ability to specify custom date/time when recording events,
enabling historical data entry. Forms show "Now - Set custom date"
with a collapsible datetime picker that converts to milliseconds.
- Add event_datetime_field() component in templates/actions.py
- Add datetime picker to all event forms (cohort, hatch, outcome,
tag, attrs, move, feed)
- Add _parse_ts_utc() helper to parse form timestamp or use current
- Add tests for backdating functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Form actions were rendering as function repr strings (e.g.,
"<function product_collected at 0x...>") instead of route paths
because the register_*_routes() pattern didn't attach .to() method
to handler functions.
Migrated all route modules to use FastHTML's APIRouter pattern:
- Routes decorated with @ar("/path") get .to() method attached
- Form(action=handler) now correctly resolves to route path
- Removed register_*_routes() functions in favor of router.to_app()
This is the idiomatic FastHTML pattern for multi-file route organization
per the official documentation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Set cookies on client instance instead of passing per-request to avoid
the deprecation warning about ambiguous cookie persistence behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The /event-log route now shows a location selector dropdown instead of
returning a 422 error when no location_id is provided. This follows the
same pattern used by the Eggs page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Simplify bottom nav from 6 to 4 items (Eggs, Feed, Move, Menu)
- Add persistent sidebar on desktop (hidden on mobile)
- Add slide-out menu drawer on mobile
- Rename Egg to Eggs with Harvest/Sell tabs (matching Feed pattern)
- Redirect /sell to /?tab=sell for consistency
- Role-gate Admin section (Locations, Status Correct) in sidebar
- Add user badge to sidebar showing username and role
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add comprehensive CLI tests for seed and serve commands
- Create README.md with development setup, deployment guide,
and environment variable reference
- Mark Step 10.3 as complete in PLAN.md
This completes the final implementation step.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive E2E acceptance tests for stats progression:
- Test #1: Baseline eggs/feed/costs with 13 ducks
- Test #2: Mixed group proration with juveniles
- Test #3: Split flock per-location stats
- Test #4: Backdated eggs use historical roster
- Test #5: Event edit with revision tracking
Tests use cumulative fixtures building on each other.
Note: Some cost_per_egg_layers values differ from spec due to
integer bird-day truncation in implementation vs fractional
in spec calculations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>