From 755c4183918f15d77c8c5d1db8d195a6465d9106 Mon Sep 17 00:00:00 2001 From: orion Date: Sun, 5 Apr 2026 09:37:14 +0000 Subject: [PATCH] feat: auto-trigger Discord wakeup when slot becomes ONGOING --- app/api/routers/calendar.py | 42 +++++++++++++++++++++++++++++++++++++ app/main.py | 5 +++++ app/models/calendar.py | 6 ++++++ 3 files changed, 53 insertions(+) diff --git a/app/api/routers/calendar.py b/app/api/routers/calendar.py index 2afa97d..d3322b0 100644 --- a/app/api/routers/calendar.py +++ b/app/api/routers/calendar.py @@ -53,6 +53,7 @@ from app.services.agent_status import ( transition_to_offline, transition_to_exhausted, ) +from app.services.discord_wakeup import create_private_wakeup_channel from app.services.minimum_workload import ( get_workload_config, get_workload_warnings_for_date, @@ -299,6 +300,45 @@ def _apply_agent_slot_update(slot: TimeSlot, payload: SlotAgentUpdate) -> None: slot.attended = True +def _maybe_trigger_discord_wakeup(db: Session, slot: TimeSlot) -> dict | None: + """Trigger Discord wakeup if slot became ONGOING and not already sent.""" + # Only trigger for ONGOING status and if not already sent + if slot.status != SlotStatus.ONGOING or slot.wakeup_sent_at is not None: + return None + + # Get user and check for discord_user_id + user = db.query(User).filter(User.id == slot.user_id).first() + if not user or not user.discord_user_id: + return None + + # Get agent for this user + agent = db.query(Agent).filter(Agent.user_id == user.id).first() + agent_id_str = agent.agent_id if agent else "unknown" + + # Build wakeup message + title = f"HarborForge Slot: {slot.event_type.value if slot.event_type else 'work'}" + message = ( + f"🎯 **Slot started**\n" + f"Agent: `{agent_id_str}`\n" + f"Type: {slot.slot_type.value}\n" + f"Duration: {slot.estimated_duration}min\n" + f"Priority: {slot.priority}\n" + f"Use `hf calendar slot {slot.id}` for details." + ) + + try: + result = create_private_wakeup_channel( + discord_user_id=user.discord_user_id, + title=title, + message=message, + ) + slot.wakeup_sent_at = datetime.now(timezone.utc) + return {"ok": True, "channel_id": result.get("channel_id")} + except Exception as e: + # Log but don't fail the slot update + return {"ok": False, "error": str(e)} + + @router.api_route( "/agent/heartbeat", methods=["GET", "POST"], @@ -338,6 +378,7 @@ def agent_update_real_slot( if slot is None: raise HTTPException(status_code=404, detail="Slot not found") _apply_agent_slot_update(slot, payload) + _maybe_trigger_discord_wakeup(db, slot) db.commit() db.refresh(slot) return TimeSlotEditResponse(slot=_slot_to_response(slot), warnings=[]) @@ -361,6 +402,7 @@ def agent_update_virtual_slot( db.rollback() raise HTTPException(status_code=404, detail="Slot not found") _apply_agent_slot_update(slot, payload) + _maybe_trigger_discord_wakeup(db, slot) db.commit() db.refresh(slot) return TimeSlotEditResponse(slot=_slot_to_response(slot), warnings=[]) diff --git a/app/main.py b/app/main.py index e8a34eb..97e4e25 100644 --- a/app/main.py +++ b/app/main.py @@ -42,6 +42,7 @@ def config_status(): return { "initialized": cfg.get("initialized", False), "backend_url": cfg.get("backend_url"), + "discord": cfg.get("discord") or {}, } except Exception: return {"initialized": False} @@ -358,6 +359,10 @@ def _migrate_schema(): ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """)) + # --- time_slots: add wakeup_sent_at for Discord wakeup tracking --- + if _has_table(db, "time_slots") and not _has_column(db, "time_slots", "wakeup_sent_at"): + db.execute(text("ALTER TABLE time_slots ADD COLUMN wakeup_sent_at DATETIME NULL")) + db.commit() except Exception as e: db.rollback() diff --git a/app/models/calendar.py b/app/models/calendar.py index 94d06f7..ea13a60 100644 --- a/app/models/calendar.py +++ b/app/models/calendar.py @@ -165,6 +165,12 @@ class TimeSlot(Base): comment="Lifecycle status of this slot", ) + wakeup_sent_at = Column( + DateTime(timezone=True), + nullable=True, + comment="When Discord wakeup was sent for this slot", + ) + plan_id = Column( Integer, ForeignKey("schedule_plans.id"),