From 880ef2b39786a0e6fd4447c574ecbfa9bebb502b Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sat, 10 Jan 2026 20:07:15 +0000 Subject: [PATCH] Fix cost/egg window to use later of first egg or first feed event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/animaltrack/web/routes/eggs.py | 25 ++++++++++++++++++++----- src/animaltrack/web/templates/eggs.py | 17 +++++++++++------ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/animaltrack/web/routes/eggs.py b/src/animaltrack/web/routes/eggs.py index d320698..10f6513 100644 --- a/src/animaltrack/web/routes/eggs.py +++ b/src/animaltrack/web/routes/eggs.py @@ -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}, } diff --git a/src/animaltrack/web/templates/eggs.py b/src/animaltrack/web/templates/eggs.py index 26e1245..7227fdd 100644 --- a/src/animaltrack/web/templates/eggs.py +++ b/src/animaltrack/web/templates/eggs.py @@ -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"),