This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/workflow_public_handlers.go
Jeffrey Smith 7adb47a50f
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 20s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-sqlite (pull_request) Successful in 2m39s
CI/CD / test-go-pg (pull_request) Failing after 2m54s
CI/CD / build-and-deploy (pull_request) Has been skipped
Feat v0.3.3 public entry + background jobs
Public workflow entry: unauthenticated routes at /api/v1/public/workflows/
for anonymous workflow participation. StartPublic creates instances with
public:<uuid> identity, ResumePublic/AdvancePublic use entry tokens.
Audience-gated: only public stages can be advanced anonymously.

SLA scanner: background goroutine (5-min interval) checks active instances
against per-stage sla_seconds. Fires workflow.sla_breach event on first
breach, marks sla_breached in instance metadata (idempotent).

Staleness sweep: new staleness_timeout_hours column on workflows. Scanner
marks idle instances as stale, cancels open assignments, fires
workflow.stale event.

New store methods: ListActiveInstances, MarkInstanceStale.
New events: workflow.sla_breach, workflow.stale.
3 new store tests (17 total), all passing on SQLite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:28:22 +00:00

141 lines
4.0 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"switchboard-core/store"
"switchboard-core/workflow"
)
// ── Public Workflow Handlers (v0.3.3) ───────
// WorkflowPublicHandler manages unauthenticated workflow entry endpoints.
type WorkflowPublicHandler struct {
engine *workflow.Engine
stores store.Stores
}
// NewWorkflowPublicHandler creates a public workflow handler.
func NewWorkflowPublicHandler(engine *workflow.Engine, stores store.Stores) *WorkflowPublicHandler {
return &WorkflowPublicHandler{engine: engine, stores: stores}
}
// publicInstanceResponse strips internal metadata from an instance for public consumption.
type publicInstanceResponse struct {
ID string `json:"id"`
WorkflowID string `json:"workflow_id"`
CurrentStage string `json:"current_stage"`
StageData json.RawMessage `json:"stage_data"`
Status string `json:"status"`
EntryToken *string `json:"entry_token,omitempty"`
CreatedAt any `json:"created_at"`
UpdatedAt any `json:"updated_at"`
}
// StartPublic creates an anonymous workflow instance.
// POST /api/v1/public/workflows/:id/start
func (h *WorkflowPublicHandler) StartPublic(c *gin.Context) {
workflowID := c.Param("id")
var body struct {
Data json.RawMessage `json:"data"`
}
if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if len(body.Data) == 0 {
body.Data = json.RawMessage(`{}`)
}
inst, err := h.engine.StartPublic(c.Request.Context(), workflowID, body.Data)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, publicInstanceResponse{
ID: inst.ID,
WorkflowID: inst.WorkflowID,
CurrentStage: inst.CurrentStage,
StageData: inst.StageData,
Status: inst.Status,
EntryToken: inst.EntryToken,
CreatedAt: inst.CreatedAt,
UpdatedAt: inst.UpdatedAt,
})
}
// ResumePublic returns an active instance by entry token.
// GET /api/v1/public/workflows/resume/:token
func (h *WorkflowPublicHandler) ResumePublic(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing token"})
return
}
inst, err := h.engine.ResumePublic(c.Request.Context(), token)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, publicInstanceResponse{
ID: inst.ID,
WorkflowID: inst.WorkflowID,
CurrentStage: inst.CurrentStage,
StageData: inst.StageData,
Status: inst.Status,
EntryToken: inst.EntryToken,
CreatedAt: inst.CreatedAt,
UpdatedAt: inst.UpdatedAt,
})
}
// AdvancePublic advances a public instance by entry token.
// POST /api/v1/public/workflows/advance/:token
func (h *WorkflowPublicHandler) AdvancePublic(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing token"})
return
}
var body struct {
Data json.RawMessage `json:"data"`
}
if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if len(body.Data) == 0 {
body.Data = json.RawMessage(`{}`)
}
inst, err := h.engine.AdvancePublic(c.Request.Context(), token, body.Data)
if err != nil {
if strings.Contains(err.Error(), "requires authenticated access") {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, publicInstanceResponse{
ID: inst.ID,
WorkflowID: inst.WorkflowID,
CurrentStage: inst.CurrentStage,
StageData: inst.StageData,
Status: inst.Status,
EntryToken: inst.EntryToken,
CreatedAt: inst.CreatedAt,
UpdatedAt: inst.UpdatedAt,
})
}