from datetime import datetime, timezone from sqlalchemy.orm import Session from backend.database import get_db_session from backend.models import Emitter, RosterUnit, IgnoredUnit def ensure_utc(dt): if dt is None: return None if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) def format_age(last_seen): if not last_seen: return "N/A" last_seen = ensure_utc(last_seen) now = datetime.now(timezone.utc) diff = now - last_seen hours = diff.total_seconds() // 3600 mins = (diff.total_seconds() % 3600) // 60 return f"{int(hours)}h {int(mins)}m" def emit_status_snapshot(): """ Merge roster (what we *intend*) with emitter data (what is *actually happening*). """ db = get_db_session() try: roster = {r.id: r for r in db.query(RosterUnit).all()} emitters = {e.id: e for e in db.query(Emitter).all()} ignored = {i.id for i in db.query(IgnoredUnit).all()} units = {} # --- Merge roster entries first --- for unit_id, r in roster.items(): e = emitters.get(unit_id) if r.retired: # Retired units get separated later status = "Retired" age = "N/A" last_seen = None fname = "" else: if e: status = e.status last_seen = ensure_utc(e.last_seen) age = format_age(last_seen) fname = e.last_file else: # Rostered but no emitter data status = "Missing" last_seen = None age = "N/A" fname = "" units[unit_id] = { "id": unit_id, "status": status, "age": age, "last": last_seen.isoformat() if last_seen else None, "fname": fname, "deployed": r.deployed, "note": r.note or "", "retired": r.retired, } # --- Add unexpected emitter-only units --- for unit_id, e in emitters.items(): if unit_id not in roster: last_seen = ensure_utc(e.last_seen) units[unit_id] = { "id": unit_id, "status": e.status, "age": format_age(last_seen), "last": last_seen.isoformat(), "fname": e.last_file, "deployed": False, # default "note": "", "retired": False, } # Separate buckets for UI active_units = { uid: u for uid, u in units.items() if not u["retired"] and u["deployed"] } benched_units = { uid: u for uid, u in units.items() if not u["retired"] and not u["deployed"] } retired_units = { uid: u for uid, u in units.items() if u["retired"] } # Unknown units - emitters that aren't in the roster and aren't ignored unknown_units = { uid: u for uid, u in units.items() if uid not in roster and uid not in ignored } return { "timestamp": datetime.utcnow().isoformat(), "units": units, "active": active_units, "benched": benched_units, "retired": retired_units, "unknown": unknown_units, "summary": { "total": len(units), "active": len(active_units), "benched": len(benched_units), "retired": len(retired_units), "unknown": len(unknown_units), "ok": sum(1 for u in units.values() if u["status"] == "OK"), "pending": sum(1 for u in units.values() if u["status"] == "Pending"), "missing": sum(1 for u in units.values() if u["status"] == "Missing"), } } finally: db.close()