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:
@@ -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:
|
||||
|
||||
@@ -198,7 +198,7 @@ class TestMoveAnimalSuccess:
|
||||
location_strip2_id,
|
||||
ducks_at_strip1,
|
||||
):
|
||||
"""Successful move returns HX-Trigger with toast."""
|
||||
"""Successful move returns session cookie with toast."""
|
||||
ts_utc = int(time.time() * 1000)
|
||||
filter_str = 'location:"Strip 1"'
|
||||
filter_ast = parse_filter(filter_str)
|
||||
@@ -219,8 +219,16 @@ class TestMoveAnimalSuccess:
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "HX-Trigger" in resp.headers
|
||||
assert "showToast" in resp.headers["HX-Trigger"]
|
||||
assert "set-cookie" in resp.headers
|
||||
session_cookie = resp.headers["set-cookie"]
|
||||
assert "session_=" in session_cookie
|
||||
# Base64 decode contains toast message
|
||||
import base64
|
||||
|
||||
cookie_value = session_cookie.split("session_=")[1].split(";")[0]
|
||||
base64_data = cookie_value.split(".")[0]
|
||||
decoded = base64.b64decode(base64_data).decode()
|
||||
assert "Moved 5 animals to Strip 2" in decoded
|
||||
|
||||
def test_move_success_resets_form(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user