feat: notifications system, webhook retry, issue assign endpoint, CLI milestones/notifications/overdue commands

This commit is contained in:
Zhi
2026-02-23 00:11:26 +00:00
parent 8e6aec8062
commit 0a8b18729b
3 changed files with 204 additions and 0 deletions

View File

@@ -268,6 +268,7 @@ def startup():
from app.models import apikey
from app.models import activity
from app.models import milestone
from app.models import notification
Base.metadata.create_all(bind=engine)
@@ -870,6 +871,7 @@ def milestone_progress(milestone_id: int, db: Session = Depends(get_db)):
# ============ Export API ============
import csv
import json
import io
from fastapi.responses import StreamingResponse
@@ -900,3 +902,115 @@ def export_issues_csv(project_id: int = None, db: Session = Depends(get_db)):
headers={"Content-Disposition": "attachment; filename=issues.csv"}
)
# ============ Notifications API ============
from app.models.notification import Notification as NotificationModel
class NotificationResponse(PydanticBaseModel):
id: int
user_id: int
type: str
title: str
message: str | None = None
entity_type: str | None = None
entity_id: int | None = None
is_read: bool
created_at: datetime
class Config:
from_attributes = True
def notify_user(db: Session, user_id: int, ntype: str, title: str, message: str = None, entity_type: str = None, entity_id: int = None):
"""Helper to create a notification."""
n = NotificationModel(user_id=user_id, type=ntype, title=title, message=message, entity_type=entity_type, entity_id=entity_id)
db.add(n)
db.commit()
return n
@app.get("/notifications", response_model=List[NotificationResponse])
def list_notifications(user_id: int, unread_only: bool = False, limit: int = 50, db: Session = Depends(get_db)):
"""List notifications for a user."""
query = db.query(NotificationModel).filter(NotificationModel.user_id == user_id)
if unread_only:
query = query.filter(NotificationModel.is_read == False)
return query.order_by(NotificationModel.created_at.desc()).limit(limit).all()
@app.get("/notifications/count")
def notification_count(user_id: int, db: Session = Depends(get_db)):
"""Get unread notification count."""
count = db.query(NotificationModel).filter(
NotificationModel.user_id == user_id,
NotificationModel.is_read == False
).count()
return {"user_id": user_id, "unread": count}
@app.post("/notifications/{notification_id}/read")
def mark_read(notification_id: int, db: Session = Depends(get_db)):
n = db.query(NotificationModel).filter(NotificationModel.id == notification_id).first()
if not n:
raise HTTPException(status_code=404, detail="Notification not found")
n.is_read = True
db.commit()
return {"status": "read"}
@app.post("/notifications/read-all")
def mark_all_read(user_id: int, db: Session = Depends(get_db)):
"""Mark all notifications as read for a user."""
db.query(NotificationModel).filter(
NotificationModel.user_id == user_id,
NotificationModel.is_read == False
).update({"is_read": True})
db.commit()
return {"status": "all_read"}
# ============ Webhook Retry ============
@app.post("/webhooks/{webhook_id}/retry/{log_id}")
def retry_webhook(webhook_id: int, log_id: int, bg: BackgroundTasks, db: Session = Depends(get_db)):
"""Retry a failed webhook delivery."""
log_entry = db.query(WebhookLog).filter(
WebhookLog.id == log_id,
WebhookLog.webhook_id == webhook_id
).first()
if not log_entry:
raise HTTPException(status_code=404, detail="Webhook log not found")
wh = db.query(Webhook).filter(Webhook.id == webhook_id).first()
if not wh:
raise HTTPException(status_code=404, detail="Webhook not found")
bg.add_task(fire_webhooks_sync, log_entry.event, json.loads(log_entry.payload), wh.project_id, db)
return {"status": "retry_queued", "log_id": log_id}
# ============ Issue Assignment with Notification ============
@app.post("/issues/{issue_id}/assign")
def assign_issue(issue_id: int, assignee_id: int, db: Session = Depends(get_db)):
"""Assign issue to user and send notification."""
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
if not issue:
raise HTTPException(status_code=404, detail="Issue not found")
user = db.query(models.User).filter(models.User.id == assignee_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
old_assignee = issue.assignee_id
issue.assignee_id = assignee_id
db.commit()
db.refresh(issue)
# Notify assignee
notify_user(db, assignee_id, "issue.assigned",
f"Issue #{issue.id} assigned to you",
f"'{issue.title}' has been assigned to you.",
"issue", issue.id)
return {"issue_id": issue.id, "assignee_id": assignee_id, "title": issue.title}