- Enable daisyUI and use btm-nav component for compact bottom navigation
- Add sticky ActionBar component for form submit buttons on mobile
- Form buttons now float above the bottom nav, preventing obscuring
- Update all form templates (actions, eggs, feed, move) to use ActionBar
- Menu drawer header now shows AnimalTrack + version like desktop sidebar
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add package-data configuration so static files (JavaScript, CSS) are
included when the package is built and deployed. Fixes 404 errors for
datetime-picker.js in production.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When egg data is imported but feed data starts later, cost/egg was
incorrectly using the egg window (e.g., 30 days) instead of the
period where both data types exist. Now cost/egg uses max(first_egg,
first_feed) to ensure accurate cost calculation.
Each metric now displays its own window: "4.7 eggs/day (30d) | €0.28/egg (7d)"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Calculate metrics from first relevant event to now (capped at 30 days)
instead of a fixed 30-day window. This fixes inaccurate metrics for new
users who have only a few days of data.
Changes:
- Add _get_first_event_ts() and _calculate_window() helpers to stats.py
- Add window_days field to EggStats dataclass
- Update routes/eggs.py and routes/feed.py to use dynamic window
- Update templates to display "N-day avg" instead of "30-day avg"
- Use ceiling division for window_days to ensure first event is included
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase event detail panel bottom padding from pb-20 to pb-28 to
prevent delete button from being obscured by mobile nav + safe area
- Change datetime picker from hx_on_click/hx_on_change to standard
onclick/onchange attributes (HTMX doesn't recognize hx-on-* syntax)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Males, juveniles, and other non-laying animals were incorrectly being
associated with egg collection events. Added life_stage='adult' and
sex='female' filters to resolve_ducks_at_location() query.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CSS for all form fields (input, textarea, select) in dark mode with
-webkit-text-fill-color for iOS Safari compatibility
- Add padding-bottom to event detail panel so delete button is visible
above bottom nav on mobile
- Display notes field in ProductCollected and ProductSold event details
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
Converts cancel buttons that use onclick="window.location.href='...'" to
proper A tags with href. This improves accessibility (keyboard navigation,
right-click options) and semantics while maintaining the same button styling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Moves ~50 lines of inline JavaScript from event_datetime_field() to a
static file. The component now uses data attributes for element binding
and global functions (toggleDatetimePicker, updateDatetimeTs) from the
static JS file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Creates slide_over_script() in shared_scripts.py that generates JavaScript
for slide-over panels with open/close functions. EventSlideOverScript and
SidebarScript now use this shared function, reducing duplicated JS logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactors 5 nearly-identical diff_panel functions into a single reusable
component. Each specific diff_panel function now delegates to the common
function with action-specific parameters.
Reduces ~300 lines of duplicated code to ~100 lines of shared logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a validation error occurs in the harvest or sell forms, the
entered field values are now preserved and redisplayed to the user.
This prevents the frustration of having to re-enter all values after
a single field fails validation.
Template changes (eggs.py):
- Added default_* parameters to harvest_form and sell_form
- Updated LabelInput/LabelTextArea fields to use these values
Route changes (routes/eggs.py):
- Updated _render_harvest_error to accept quantity and notes
- Updated _render_sell_error to accept quantity, total_price_cents,
buyer, and notes
- All error return calls now pass form values through
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add hx_disabled_elt="this" to submit buttons across all forms to
disable them during form submission, preventing double-clicks and
providing visual feedback that the action is processing.
Buttons updated:
- actions.py: promote, cohort, hatch, tag-add, tag-end, attrs,
outcome, status-correct forms and their diff_panel confirmations
- eggs.py: collect and sell forms
- feed.py: give and purchase forms
- locations.py: create and rename forms
- move.py: move form and diff_panel confirmation
- products.py: create form
- registry.py: filter apply button
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MonsterUI LabelSelect has a confirmed bug where it sends the option's
label text instead of the value attribute on form submission. This was
causing 422 validation errors in forms.
- Replace all LabelSelect usages with raw Div(FormLabel, Select) pattern
- Add comments documenting the MonsterUI bug workaround
- Matches pattern already used in feed.py since commit ad1f910
Files modified: eggs.py, move.py, actions.py, products.py
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable recording "checked coop, found 0 eggs" to distinguish from days
when the coop wasn't checked at all. Statistics remain eggs/calendar day.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug fix: Stats queries (eggs/day, feed/bird/day, etc.) were not excluding
tombstoned (deleted) events. Updated EventStore.list_events() to exclude
tombstoned events by default via LEFT JOIN, and updated direct SQL queries
in stats.py with the same tombstone exclusion.
New stats added:
- Harvest form: cost/egg (global, 30-day avg)
- Sell form: avg price/egg (30-day)
- Give feed form: cost/bird/day (global, 30-day avg)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create recent_events.py helper for rendering event lists with humanized
timestamps and deleted event styling (line-through + opacity)
- Query events with ORDER BY ts_utc DESC to show newest first
- Join event_tombstones to detect deleted events
- Fix move form to read animal_ids (not resolved_ids) from entity_refs
- Fix feed purchase format to use total_kg from entity_refs
- Use hx_get with #event-panel-content target for slide-over panel
- Add days-since-last stats for move and feed forms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The delete event functionality uses vanilla fetch() instead of HTMX,
so it wasn't getting the x-csrf-token header that the htmx:configRequest
listener adds. This caused 403 Forbidden on event deletion.
Changes:
- Made getCsrfToken() a global window function so it can be used by
both HTMX and vanilla fetch() calls
- Added x-csrf-token header to the deleteEvent() fetch request
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
FastHTML's fast_app() silently ignores the 'after' parameter - it only
supports 'before'. The afterware function was never being called, so the
CSRF cookie was never set, causing 403 Forbidden on all POST requests
in production.
Replaced the non-functional afterware with proper Starlette ASGI
middleware (CsrfCookieMiddleware) that intercepts responses and adds
the Set-Cookie header directly to HTML GET responses.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add global CSS to remove ::marker pseudo-elements from uk-tab and
uk-switcher components. Also clean up tab structure to match MonsterUI
idioms (uk-active on Li, use None instead of empty string for cls).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use Grid with col-span instead of flex to give the filter input 10/12
of the width and the Apply button only 2/12.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Display commit date and short hash (e.g., "2026-01-08 fb59ef7") below
the ANIMALTRACK title in the sidebar. In development, reads from git
directly; in Docker, reads from BUILD_DATE/BUILD_COMMIT env vars
injected at build time from the Nix flake.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
FastHTML omits the value attribute when value="" (empty string), causing
browsers to use the option's text content as the submitted value. This
made forms send "Keep current" or "No change" text instead of empty
string, failing Pydantic enum validation.
Fixed by using "-" as a sentinel value instead of "" for "no change"
options, and updating route handlers to treat "-" as None.
Affected forms:
- Promote form (sex, repro_status)
- Update attributes form (sex, life_stage, repro_status)
- Outcome form (yield_product_code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Both forms now have datetime pickers like the feed forms, allowing
users to record events at past timestamps. Each form has a unique
field_id (harvest_datetime, sell_datetime) to avoid conflicts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>