Feat v0.7.9 workflow independence (#63)
All checks were successful
CI/CD / detect-changes (push) Successful in 20s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 25s
CI/CD / test-sqlite (push) Successful in 2m51s
CI/CD / test-go-pg (push) Successful in 3m0s
CI/CD / build-and-deploy (push) Successful in 1m26s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #63.
This commit is contained in:
2026-04-02 22:31:47 +00:00
committed by xcaliber
parent 3cdfdcf943
commit a9cf71b76d
27 changed files with 1324 additions and 4205 deletions

View File

@@ -25,6 +25,7 @@ import (
"github.com/gin-gonic/gin"
"armature/config"
"armature/models"
"armature/store"
)
@@ -631,18 +632,20 @@ func (e *Engine) RenderLogin() gin.HandlerFunc {
// WorkflowPageData is passed to the workflow.html template via PageData.Data.
type WorkflowPageData struct {
ChannelID string
ChannelTitle string
ChannelDescription string
SessionID string
SessionName string
StageMode string // custom | form_only | form_chat | review
StageName string
FormTemplateJSON string // typed form template JSON (empty if custom)
TotalStages int
CurrentStage int
SurfacePkgID string
BrandingJSON string
EntryToken string
WorkflowTitle string
WorkflowDescription string
SessionID string
SessionName string
StageMode string // form | review | delegated | automated
StageName string
FormTemplateJSON string // typed form template JSON (empty if delegated)
TotalStages int
CurrentStage int
SurfacePkgID string
BrandingJSON string
InstanceID string // workflow instance ID (used for API calls)
Status string // pending | active | completed | cancelled | stale
}
// WorkflowLandingPageData is passed to workflow-landing.html.
@@ -660,49 +663,106 @@ type WorkflowLandingPageData struct {
PersonaName string
PersonaIcon string
StageCount int
FirstStageMode string // custom | form_only | form_chat
FirstStageMode string // form | review | delegated | automated
ResumeURL string // non-empty if visitor has an active session
}
// RenderWorkflow renders the workflow entry page for anonymous session visitors.
// RenderWorkflow renders the workflow execution page for a running instance.
// Route: /w/:id — :id is either an instance ID or a public entry token.
// The middleware (AuthOrSession) has already created/resumed the session.
func (e *Engine) RenderWorkflow() gin.HandlerFunc {
return func(c *gin.Context) {
// :id is the public entry token (from /w/:id)
channelID := c.Param("id")
ctx := c.Request.Context()
routeID := c.Param("id")
sessionID := c.GetString("session_id")
var title, description string
if title == "" {
title = "Workflow"
}
// Load session display name
sessionName := "Visitor"
// Load workflow stage info for form rendering
var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
var totalStages, currentStage int
stageMode = "custom" // default
instanceName, _, _ := e.loadBranding()
// Try to load instance — the route param may be an instance ID
// or an entry token. Also check the ?token= query param.
var inst *models.WorkflowInstance
if e.stores.Workflows != nil {
// First try: query param token (from landing page redirect)
if qToken := c.Query("token"); qToken != "" {
inst, _ = e.stores.Workflows.GetInstanceByToken(ctx, qToken)
}
// Second try: route param as entry token
if inst == nil {
inst, _ = e.stores.Workflows.GetInstanceByToken(ctx, routeID)
}
// Third try: route param as instance ID
if inst == nil {
inst, _ = e.stores.Workflows.GetInstance(ctx, routeID)
}
}
if inst == nil {
c.String(http.StatusNotFound, "Workflow instance not found")
return
}
// Load workflow definition for title, description, branding
title, description := "Workflow", ""
var brandingJSON string
wf, _ := e.stores.Workflows.GetByID(ctx, inst.WorkflowID)
if wf != nil {
title = wf.Name
description = wf.Description
if len(wf.Branding) > 0 {
brandingJSON = string(wf.Branding)
}
}
// Load stages to resolve current stage info
var stageMode, stageName, formTplJSON, surfacePkgID string
var totalStages, currentStage int
stageMode = "form" // default
stages, _ := e.stores.Workflows.ListStages(ctx, inst.WorkflowID)
totalStages = len(stages)
for i, s := range stages {
if s.ID == inst.CurrentStage || s.Name == inst.CurrentStage {
currentStage = i
stageName = s.Name
stageMode = s.StageMode
if stageMode == "" {
stageMode = "form"
}
if len(s.FormTemplate) > 0 && string(s.FormTemplate) != "null" {
formTplJSON = string(s.FormTemplate)
}
if s.SurfacePkgID != nil {
surfacePkgID = *s.SurfacePkgID
}
break
}
}
// Resolve the actual entry token from the instance (used by JS API calls)
entryToken := ""
if inst.EntryToken != nil {
entryToken = *inst.EntryToken
}
e.Render(c, "workflow.html", PageData{
Surface: "workflow",
InstanceName: instanceName,
Data: WorkflowPageData{
ChannelID: channelID,
ChannelTitle: title,
ChannelDescription: description,
SessionID: sessionID,
SessionName: sessionName,
StageMode: stageMode,
StageName: stageName,
FormTemplateJSON: formTplJSON,
TotalStages: totalStages,
CurrentStage: currentStage,
SurfacePkgID: surfacePkgID,
BrandingJSON: brandingJSON,
EntryToken: entryToken,
WorkflowTitle: title,
WorkflowDescription: description,
SessionID: sessionID,
SessionName: sessionName,
StageMode: stageMode,
StageName: stageName,
FormTemplateJSON: formTplJSON,
TotalStages: totalStages,
CurrentStage: currentStage,
SurfacePkgID: surfacePkgID,
BrandingJSON: brandingJSON,
InstanceID: inst.ID,
Status: inst.Status,
},
})
}