feat: add event detail slide-over, fix toasts, and checkbox selection

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>
This commit is contained in:
2026-01-01 19:10:57 +00:00
parent 25a91c3322
commit 3937d675ba
19 changed files with 1420 additions and 360 deletions

View File

@@ -149,7 +149,7 @@ class TestCohortCreationSuccess:
assert count_after == count_before + 3
def test_cohort_success_returns_toast(self, client, seeded_db, location_strip1_id):
"""Successful cohort creation returns HX-Trigger with toast."""
"""Successful cohort creation stores toast in session."""
resp = client.post(
"/actions/animal-cohort",
data={
@@ -164,8 +164,20 @@ class TestCohortCreationSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie (FastHTML's add_toast mechanism)
# The session cookie contains base64-encoded toast data with "toasts" key
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
# Base64 decode contains toast message (eyJ0b2FzdHMi... = {"toasts"...)
import base64
# Extract base64 portion from cookie value
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
# FastHTML uses itsdangerous, so format is base64.timestamp.signature
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Created 2 duck" in decoded
class TestCohortCreationValidation:
@@ -363,7 +375,7 @@ class TestHatchRecordingSuccess:
assert count_at_nursery >= 3
def test_hatch_success_returns_toast(self, client, seeded_db, location_strip1_id):
"""Successful hatch recording returns HX-Trigger with toast."""
"""Successful hatch recording stores toast in session."""
resp = client.post(
"/actions/hatch-recorded",
data={
@@ -375,8 +387,16 @@ class TestHatchRecordingSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie (FastHTML's add_toast mechanism)
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
import base64
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Recorded 2 hatchling" in decoded
class TestHatchRecordingValidation:
@@ -709,7 +729,8 @@ class TestTagAddSuccess:
assert tag_count >= len(animals_for_tagging)
def test_tag_add_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful tag add returns HX-Trigger with toast."""
"""Successful tag add stores toast in session."""
import base64
import time
from animaltrack.selection import compute_roster_hash
@@ -730,8 +751,14 @@ class TestTagAddSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Tagged" in decoded and "test-tag-toast" in decoded
class TestTagAddValidation:
@@ -898,7 +925,8 @@ class TestTagEndSuccess:
assert open_after == 0
def test_tag_end_success_returns_toast(self, client, seeded_db, tagged_animals):
"""Successful tag end returns HX-Trigger with toast."""
"""Successful tag end stores toast in session."""
import base64
import time
from animaltrack.selection import compute_roster_hash
@@ -919,8 +947,14 @@ class TestTagEndSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Ended tag" in decoded and "test-end-tag" in decoded
class TestTagEndValidation:
@@ -1069,7 +1103,8 @@ class TestAttrsSuccess:
assert adult_count == len(animals_for_tagging)
def test_attrs_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful attrs update returns HX-Trigger with toast."""
"""Successful attrs update stores toast in session."""
import base64
import time
from animaltrack.selection import compute_roster_hash
@@ -1090,8 +1125,14 @@ class TestAttrsSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Updated attributes" in decoded
class TestAttrsValidation:
@@ -1239,7 +1280,8 @@ class TestOutcomeSuccess:
assert harvested_count == len(animals_for_tagging)
def test_outcome_success_returns_toast(self, client, seeded_db, animals_for_tagging):
"""Successful outcome recording returns HX-Trigger with toast."""
"""Successful outcome recording stores toast in session."""
import base64
import time
from animaltrack.selection import compute_roster_hash
@@ -1260,8 +1302,14 @@ class TestOutcomeSuccess:
)
assert resp.status_code == 200
assert "HX-Trigger" in resp.headers
assert "showToast" in resp.headers["HX-Trigger"]
# Toast is stored in session cookie
assert "set-cookie" in resp.headers
session_cookie = resp.headers["set-cookie"]
assert "session_=" in session_cookie
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
base64_data = cookie_value.split(".")[0]
decoded = base64.b64decode(base64_data).decode()
assert "Recorded sold" in decoded
class TestOutcomeValidation: