diff --git a/src/aeros_simulation/service.py b/src/aeros_simulation/service.py index ab3770b..4f573fd 100644 --- a/src/aeros_simulation/service.py +++ b/src/aeros_simulation/service.py @@ -1046,42 +1046,35 @@ async def get_forecast_trending( *, db_session: DbSession, time_range: str, limit: int = 12 ) -> list: """ - Return EAF/EFOR/SOF/EDH for the last `limit` completed weekly or monthly - forecast simulations, oldest first (for chronological chart rendering). + Return EAF/EFOR/SOF/EDH for the last `limit` completed simulations. Strategy: - 1. Try simulations named Simulation_weekly_* / Simulation_monthly_* (scheduled forecasts). - 2. If fewer than `limit` found, fall back to any completed simulation, grouped - by week or month bucket (latest sim per bucket), so the chart is populated - even before automated forecast simulations exist. + 1. Try simulations named Simulation_weekly_* / Simulation_monthly_* (forecast schedule). + 2. If fewer than `limit` found, fall back to any completed simulation grouped by + week/month bucket so the chart is always populated. + + Each simulation's plant metrics are fetched via get_plant_calc_result (same path the + dashboard uses) rather than a JOIN, avoiding silent row-drops when CalcResults aren't + yet committed under the plant node name. """ - plant_node_name = "- TJB - Unit 3 -" name_prefix = f"Simulation_{time_range}_" - base_join = ( - select(AerosSimulation, AerosSimulationCalcResult) - .join( - AerosSimulationCalcResult, - AerosSimulationCalcResult.aeros_simulation_id == AerosSimulation.id, + async def _sims_query(extra_where=None): + q = ( + select(AerosSimulation) + .where(AerosSimulation.status == "completed") + .order_by(AerosSimulation.completed_at.desc()) ) - .join(AerosNode, AerosNode.id == AerosSimulationCalcResult.aeros_node_id) - .where(AerosSimulation.status == "completed") - .where(AerosNode.node_name == plant_node_name) - ) - - # First pass: named forecast simulations - named_q = ( - base_join - .where(AerosSimulation.simulation_name.like(f"{name_prefix}%")) - .order_by(AerosSimulation.completed_at.desc()) - .limit(limit) - ) - result = await db_session.execute(named_q) - named_rows = result.all() - - def _format(rows): - return [ - { + if extra_where is not None: + q = q.where(extra_where) + return q + + async def _fetch_plant(sim) -> dict | None: + try: + calc = await get_plant_calc_result(db_session=db_session, simulation_id=sim.id) + if calc is None: + return None + return { "date": sim.completed_at, "simulation_id": str(sim.id), "simulation_name": sim.simulation_name, @@ -1090,32 +1083,48 @@ async def get_forecast_trending( "sof": calc.sof, "edh": calc.derating_hours, } - for sim, calc in rows - ] - - if len(named_rows) >= limit: - return _format(reversed(named_rows)) + except Exception: + return None - # Fallback: any completed sim, group by week/month bucket (latest per bucket) - fallback_q = ( - base_join - .order_by(AerosSimulation.completed_at.desc()) - .limit(limit * 20) + # --- Pass 1: named forecast simulations --- + named_q = await _sims_query( + AerosSimulation.simulation_name.like(f"{name_prefix}%") ) + named_q = named_q.limit(limit) + result = await db_session.execute(named_q) + named_sims = result.scalars().all() + + points = [] + for sim in reversed(named_sims): # oldest → newest + pt = await _fetch_plant(sim) + if pt: + points.append(pt) + + if len(points) >= limit: + return points + + # --- Pass 2: any completed sim, bucket by week/month --- + fallback_q = await _sims_query() + fallback_q = fallback_q.limit(limit * 20) result = await db_session.execute(fallback_q) - all_rows = result.all() + all_sims = result.scalars().all() buckets: dict = {} - for sim, calc in all_rows: + for sim in all_sims: ts = sim.completed_at if ts is None: continue - if time_range == "weekly": - bucket = ts.strftime("%G-W%V") # ISO week - else: - bucket = ts.strftime("%Y-%m") + bucket = ts.strftime("%G-W%V") if time_range == "weekly" else ts.strftime("%Y-%m") if bucket not in buckets: - buckets[bucket] = (sim, calc) + buckets[bucket] = sim + + sorted_sims = [sim for _, sim in sorted(buckets.items(), key=lambda x: x[0])] + sorted_sims = sorted_sims[-limit:] # keep last `limit` buckets + + fallback_points = [] + for sim in sorted_sims: + pt = await _fetch_plant(sim) + if pt: + fallback_points.append(pt) - sorted_items = sorted(buckets.items(), key=lambda x: x[0])[-limit:] - return _format((sim, calc) for _, (sim, calc) in sorted_items) + return fallback_points if fallback_points else points