Feat v0.7.10 workflow handoff + assignment UI
Some checks failed
CI/CD / test-sqlite (pull_request) Has been cancelled
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Has been cancelled
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / e2e-smoke (pull_request) Has been cancelled

Public→team stage handoff with audience mismatch detection, enriched
assignment API responses (workflow_name, stage_name, sla_breached),
team inbox with claim/assign actions, assignment notifications, team
middleware system-admin bypass fix, and SDK gap closure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 23:15:40 +00:00
parent a9cf71b76d
commit 8f8c1b0e53
17 changed files with 818 additions and 36 deletions

View File

@@ -100,6 +100,8 @@ func (e *Engine) Start(ctx context.Context, workflowID string, initialData json.
}
if err := e.stores.Workflows.CreateAssignment(ctx, a); err != nil {
log.Printf("[workflow-engine] create initial assignment: %v", err)
} else {
e.notifyAssignment(ctx, a, wf.Name, stages[0].Name)
}
}
@@ -247,6 +249,12 @@ func (e *Engine) advanceInternal(ctx context.Context, instanceID string, stageDa
"assignment_id": a.ID,
"team_id": a.TeamID,
})
// Notify team members
wfTitle := ""
if wf, _ := e.stores.Workflows.GetByID(ctx, inst.WorkflowID); wf != nil {
wfTitle = wf.Name
}
e.notifyAssignment(ctx, a, wfTitle, nextStage.Name)
}
}
@@ -477,6 +485,45 @@ func (e *Engine) emit(_ context.Context, label, room string, payload map[string]
})
}
// notifyAssignment creates notifications for team members when a workflow
// assignment is created. If the assignment targets a specific user, only
// that user is notified. Otherwise all team members are notified.
func (e *Engine) notifyAssignment(ctx context.Context, a *models.WorkflowAssignment, workflowTitle, stageName string) {
if e.stores.Notifications == nil || e.stores.Teams == nil {
return
}
var recipients []string
if a.AssignedTo != nil && *a.AssignedTo != "" {
recipients = []string{*a.AssignedTo}
} else {
members, err := e.stores.Teams.ListMembers(ctx, a.TeamID)
if err != nil {
log.Printf("[workflow-engine] list team members for notification: %v", err)
return
}
for _, m := range members {
recipients = append(recipients, m.UserID)
}
}
title := "Workflow assignment: " + stageName
body := workflowTitle
for _, uid := range recipients {
n := &models.Notification{
UserID: uid,
Type: models.NotifTypeWorkflowAssign,
Title: title,
Body: body,
ResourceType: "workflow",
ResourceID: a.InstanceID,
}
if err := e.stores.Notifications.Create(ctx, n); err != nil {
log.Printf("[workflow-engine] create notification: %v", err)
}
}
}
// mergeJSON merges b into a (shallow). Returns a if b is empty.
func mergeJSON(a, b json.RawMessage) json.RawMessage {
if len(b) == 0 || string(b) == "{}" || string(b) == "null" {