Fix cost/egg window to use later of first egg or first feed event
All checks were successful
Deploy / deploy (push) Successful in 1m39s

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>
This commit is contained in:
2026-01-10 20:07:15 +00:00
parent 86dc3a13d2
commit 880ef2b397
2 changed files with 31 additions and 11 deletions

View File

@@ -184,7 +184,8 @@ def _get_global_cost_per_egg(db: Any, now_ms: int) -> tuple[float | None, int]:
"""Calculate global cost per egg over dynamic window.
Aggregates feed costs and egg counts across all locations.
Uses a dynamic window based on the first egg collection event.
Uses a dynamic window based on the later of first egg event or first feed event,
ensuring we only calculate cost for periods with complete data.
Args:
db: Database connection.
@@ -195,9 +196,22 @@ def _get_global_cost_per_egg(db: Any, now_ms: int) -> tuple[float | None, int]:
"""
from animaltrack.events import FEED_GIVEN
# Calculate dynamic window based on first egg event
# Calculate dynamic window based on the later of first egg or first feed event
# This ensures we only calculate cost/egg for periods with both data types
first_egg_ts = _get_first_event_ts(db, "ProductCollected", product_prefix="egg.")
window_start, window_end, window_days = _calculate_window(now_ms, first_egg_ts)
first_feed_ts = _get_first_event_ts(db, "FeedGiven")
# Use the later timestamp (max) to ensure complete data for both metrics
if first_egg_ts is None and first_feed_ts is None:
first_event_ts = None
elif first_egg_ts is None:
first_event_ts = first_feed_ts
elif first_feed_ts is None:
first_event_ts = first_egg_ts
else:
first_event_ts = max(first_egg_ts, first_feed_ts)
window_start, window_end, window_days = _calculate_window(now_ms, first_event_ts)
event_store = EventStore(db)
@@ -300,17 +314,18 @@ def _get_eggs_display_data(db: Any, locations: list) -> dict:
Returns:
Dict with harvest_events, sell_events, eggs_per_day, cost_per_egg,
eggs_window_days, sales_stats, location_names.
eggs_window_days, cost_window_days, sales_stats, location_names.
"""
now_ms = int(time.time() * 1000)
eggs_per_day, eggs_window_days = _get_eggs_per_day(db, now_ms)
cost_per_egg, _ = _get_global_cost_per_egg(db, now_ms)
cost_per_egg, cost_window_days = _get_global_cost_per_egg(db, now_ms)
return {
"harvest_events": _get_recent_events(db, PRODUCT_COLLECTED, limit=10),
"sell_events": _get_recent_events(db, PRODUCT_SOLD, limit=10),
"eggs_per_day": eggs_per_day,
"cost_per_egg": cost_per_egg,
"eggs_window_days": eggs_window_days,
"cost_window_days": cost_window_days,
"sales_stats": _get_sales_stats(db, now_ms),
"location_names": {loc.id: loc.name for loc in locations},
}

View File

@@ -36,6 +36,7 @@ def eggs_page(
eggs_per_day: float | None = None,
cost_per_egg: float | None = None,
eggs_window_days: int = 30,
cost_window_days: int = 30,
sales_stats: dict | None = None,
location_names: dict[str, str] | None = None,
# Field value preservation on errors
@@ -62,7 +63,8 @@ def eggs_page(
sell_events: Recent ProductSold events (most recent first).
eggs_per_day: Average eggs per day over window.
cost_per_egg: Average cost per egg in EUR over window.
eggs_window_days: Actual window size in days for the metrics.
eggs_window_days: Actual window size in days for eggs_per_day.
cost_window_days: Actual window size in days for cost_per_egg.
sales_stats: Dict with 'total_qty', 'total_cents', and 'avg_price_per_egg_cents'.
location_names: Dict mapping location_id to location name for display.
harvest_quantity: Preserved quantity value on error.
@@ -100,6 +102,7 @@ def eggs_page(
eggs_per_day=eggs_per_day,
cost_per_egg=cost_per_egg,
eggs_window_days=eggs_window_days,
cost_window_days=cost_window_days,
location_names=location_names,
default_quantity=harvest_quantity,
default_notes=harvest_notes,
@@ -135,6 +138,7 @@ def harvest_form(
eggs_per_day: float | None = None,
cost_per_egg: float | None = None,
eggs_window_days: int = 30,
cost_window_days: int = 30,
location_names: dict[str, str] | None = None,
default_quantity: str | None = None,
default_notes: str | None = None,
@@ -149,7 +153,8 @@ def harvest_form(
recent_events: Recent (Event, is_deleted) tuples, most recent first.
eggs_per_day: Average eggs per day over window.
cost_per_egg: Average cost per egg in EUR over window.
eggs_window_days: Actual window size in days for the metrics.
eggs_window_days: Actual window size in days for eggs_per_day.
cost_window_days: Actual window size in days for cost_per_egg.
location_names: Dict mapping location_id to location name for display.
default_quantity: Preserved quantity value on error.
default_notes: Preserved notes value on error.
@@ -193,13 +198,13 @@ def harvest_form(
loc_name = location_names.get(loc_id, "Unknown")
return f"{quantity} eggs from {loc_name}", event.id
# Build stats text - combine eggs/day and cost/egg
# Build stats text - each metric shows its own window
stat_parts = []
if eggs_per_day is not None:
stat_parts.append(f"{eggs_per_day:.1f} eggs/day")
stat_parts.append(f"{eggs_per_day:.1f} eggs/day ({eggs_window_days}d)")
if cost_per_egg is not None:
stat_parts.append(f"{cost_per_egg:.3f}/egg cost")
stat_text = " | ".join(stat_parts) + f" ({eggs_window_days}-day avg)" if stat_parts else None
stat_parts.append(f"{cost_per_egg:.3f}/egg ({cost_window_days}d)")
stat_text = " | ".join(stat_parts) if stat_parts else None
form = Form(
H2("Harvest Eggs", cls="text-xl font-bold mb-4"),