HarborForge.Backend: dev-2026-03-29 -> main #13
@@ -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=[])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user