- 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>
Add user_defaults table and repository for persisting form defaults
across sessions. Feed and egg forms now load/save user preferences.
Changes:
- Add migration 0009-user-defaults.sql with table schema
- Add UserDefault model and UserDefaultsRepository
- Integrate defaults into feed route (location, feed_type, amount)
- Integrate defaults into egg route (location)
- Add repository unit tests and route integration tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add POST /actions/product-sold route for recording product sales.
Changes:
- Create web/templates/products.py with product_sold_form
- Create web/routes/products.py with GET /sell and POST /actions/product-sold
- Wire up routes in __init__.py and app.py
- Add "Record Sale" link to Egg Quick Capture page
- Add comprehensive tests for form rendering and sale recording
The form allows selling any sellable product with quantity and price,
and calculates unit_price_cents using floor division. Defaults to
egg.duck product as per spec.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>