Add recent events and stats to eggs, feed, and move forms
All checks were successful
Deploy / deploy (push) Successful in 2m40s

- 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>
This commit is contained in:
2026-01-08 21:10:09 +00:00
parent 62cc6c07d1
commit e42eede010
11 changed files with 1102 additions and 33 deletions

View File

@@ -472,3 +472,116 @@ class TestMoveAnimalMismatch:
payload = json.loads(event_row[0])
# Should have moved 3 animals (5 original - 2 moved by client B)
assert len(payload["resolved_ids"]) == 3
class TestMoveRecentEvents:
"""Tests for recent events display on move page."""
def test_move_form_shows_recent_events_section(self, client):
"""Move form shows Recent Moves section."""
resp = client.get("/move")
assert resp.status_code == 200
assert "Recent Moves" in resp.text
def test_move_event_appears_in_recent(
self,
client,
seeded_db,
animal_service,
location_strip1_id,
location_strip2_id,
ducks_at_strip1,
):
"""Newly created move event appears in recent events list."""
ts_utc = int(time.time() * 1000)
filter_str = 'location:"Strip 1"'
filter_ast = parse_filter(filter_str)
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
resp = client.post(
"/actions/animal-move",
data={
"filter": filter_str,
"to_location_id": location_strip2_id,
"resolved_ids": resolution.animal_ids,
"roster_hash": roster_hash,
"from_location_id": location_strip1_id,
"ts_utc": str(ts_utc),
"nonce": "test-nonce-recent-move-1",
},
)
assert resp.status_code == 200
# Recent events should include the newly created event
assert "/events/" in resp.text
def test_move_event_links_to_detail(
self,
client,
seeded_db,
animal_service,
location_strip1_id,
location_strip2_id,
ducks_at_strip1,
):
"""Move events in recent list link to event detail page."""
ts_utc = int(time.time() * 1000)
filter_str = 'location:"Strip 1"'
filter_ast = parse_filter(filter_str)
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
resp = client.post(
"/actions/animal-move",
data={
"filter": filter_str,
"to_location_id": location_strip2_id,
"resolved_ids": resolution.animal_ids,
"roster_hash": roster_hash,
"from_location_id": location_strip1_id,
"ts_utc": str(ts_utc),
"nonce": "test-nonce-recent-move-2",
},
)
assert resp.status_code == 200
# Get the event ID from DB
event_row = seeded_db.execute(
"SELECT id FROM events WHERE type = 'AnimalMoved' ORDER BY id DESC LIMIT 1"
).fetchone()
event_id = event_row[0]
# The response should contain a link to the event detail
assert f"/events/{event_id}" in resp.text
def test_days_since_last_move_shows_today(
self,
client,
seeded_db,
animal_service,
location_strip1_id,
location_strip2_id,
ducks_at_strip1,
):
"""After a move today, shows 'Last move: today'."""
ts_utc = int(time.time() * 1000)
filter_str = 'location:"Strip 1"'
filter_ast = parse_filter(filter_str)
resolution = resolve_filter(seeded_db, filter_ast, ts_utc)
roster_hash = compute_roster_hash(resolution.animal_ids, location_strip1_id)
resp = client.post(
"/actions/animal-move",
data={
"filter": filter_str,
"to_location_id": location_strip2_id,
"resolved_ids": resolution.animal_ids,
"roster_hash": roster_hash,
"from_location_id": location_strip1_id,
"ts_utc": str(ts_utc),
"nonce": "test-nonce-recent-move-3",
},
)
assert resp.status_code == 200
# Stats should show "Last move: today"
assert "Last move: today" in resp.text