Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
585 lines
20 KiB
Markdown
585 lines
20 KiB
Markdown
# ROADMAP-0.38 — Workflow Product Maturity
|
|
|
|
Everything deferred from v0.37.15 plus what the MVP gate actually
|
|
requires: "Team admins build workflows **visually**."
|
|
|
|
v0.37.15 delivers the plumbing — cancel, unclaim, reassign, stage CRUD
|
|
in the FE, queue views. This series builds the product on top of that
|
|
plumbing.
|
|
|
|
---
|
|
|
|
## Series Overview
|
|
|
|
```
|
|
v0.37.15 Workflow Ownership & Lifecycle (plumbing)
|
|
v0.37.16 Projects Surface
|
|
v0.37.17 Debug / Model Surface
|
|
│
|
|
── 0.37 tag ──
|
|
│
|
|
v0.38.0 Form Builder ← team admins stop writing JSON
|
|
v0.38.1 Stage Reorder + Bulk Ops ← queue management at scale
|
|
v0.38.2 SLA Alerting ← stale queues self-report
|
|
v0.38.3 Visitor Experience ← the public face
|
|
v0.38.4 Workflow Templates + Clone ← reuse across teams
|
|
v0.38.5 Analytics Dashboard ← throughput, bottlenecks, SLA compliance
|
|
│
|
|
─ ─ ─ MVP v0.50.0 gate ─ ─ ─
|
|
```
|
|
|
|
**Milestone gates:**
|
|
|
|
| Milestone | Gate | Meaning |
|
|
|-----------|------|---------|
|
|
| v0.37 tag | **UI Complete** | Every platform capability has a surface; no features require raw API calls |
|
|
| v0.38.0 | **Self-service** | A team admin can build a complete workflow without touching JSON or asking a global admin |
|
|
| v0.38.2 | **Operational** | Stale work surfaces automatically; team admins can manage queues without monitoring dashboards |
|
|
| v0.38.5 | **Measurable** | Team leads can answer "how long does stage X take" and "where are we bottlenecked" |
|
|
|
|
---
|
|
|
|
## v0.38.0 — Form Builder
|
|
|
|
**The single biggest gap.** `form_template` is currently raw JSON in a
|
|
textarea. The MVP gate says "build workflows visually" — this is the
|
|
make-or-break.
|
|
|
|
### Scope
|
|
|
|
A visual form builder embedded in the stage editor (E2 from the .15
|
|
design). Not a standalone surface — it's a panel that opens when you
|
|
click "Edit Form" on a `form_only` or `form_chat` stage.
|
|
|
|
### Fields supported (matching existing TypedFormTemplate)
|
|
|
|
`text`, `email`, `select`, `number`, `date`, `textarea`, `checkbox`,
|
|
`file`. Each with the validation rules already defined in
|
|
`models/workflow.go` (min/max length, pattern, min/max value, etc).
|
|
|
|
### JSX Exemplar
|
|
|
|
```jsx
|
|
/**
|
|
* FormBuilder — visual editor for stage form_template
|
|
*
|
|
* Opens as a slide-out panel from StageEditor.
|
|
* Produces a TypedFormTemplate JSON blob on save.
|
|
*
|
|
* Features:
|
|
* - Add/remove/reorder fields
|
|
* - Field type picker with type-specific validation config
|
|
* - Fieldset grouping (progressive multi-step forms)
|
|
* - Conditional visibility (when/op/value)
|
|
* - Live preview pane
|
|
*/
|
|
function FormBuilder({ initial, onSave, onCancel }) {
|
|
const [fields, setFields] = useState(initial?.fields || []);
|
|
const [fieldsets, setFieldsets] = useState(initial?.fieldsets || []);
|
|
const [useFieldsets, setUseFieldsets] = useState((initial?.fieldsets?.length || 0) > 0);
|
|
const [preview, setPreview] = useState(false);
|
|
|
|
function addField(targetFieldset) {
|
|
const field = {
|
|
key: `field_${Date.now()}`,
|
|
type: 'text',
|
|
label: '',
|
|
required: false,
|
|
};
|
|
if (useFieldsets && targetFieldset !== undefined) {
|
|
// Add to specific fieldset
|
|
setFieldsets(fs => fs.map((f, i) =>
|
|
i === targetFieldset ? { ...f, fields: [...f.fields, field] } : f
|
|
));
|
|
} else {
|
|
setFields(f => [...f, field]);
|
|
}
|
|
}
|
|
|
|
function save() {
|
|
const template = useFieldsets
|
|
? { fieldsets, hooks: initial?.hooks || null }
|
|
: { fields, hooks: initial?.hooks || null };
|
|
onSave(template);
|
|
}
|
|
|
|
return (
|
|
<div className="form-builder">
|
|
<div className="form-builder__header">
|
|
<h4>Form Builder</h4>
|
|
<label className="toggle-label">
|
|
<input type="checkbox" checked={useFieldsets}
|
|
onChange={e => setUseFieldsets(e.target.checked)} />
|
|
Multi-step (fieldsets)
|
|
</label>
|
|
<button className="btn-small" onClick={() => setPreview(!preview)}>
|
|
{preview ? 'Edit' : 'Preview'}
|
|
</button>
|
|
</div>
|
|
|
|
{preview ? (
|
|
<FormPreview fields={fields} fieldsets={useFieldsets ? fieldsets : null} />
|
|
) : (
|
|
<div className="form-builder__canvas">
|
|
{useFieldsets ? (
|
|
<FieldsetEditor fieldsets={fieldsets} setFieldsets={setFieldsets} />
|
|
) : (
|
|
<FieldList fields={fields} setFields={setFields} />
|
|
)}
|
|
<button className="btn-small" onClick={() => addField()}>
|
|
+ Add Field
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="form-builder__footer">
|
|
<button className="btn-small btn-primary" onClick={save}>Save Form</button>
|
|
<button className="btn-small" onClick={onCancel}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FieldEditor — single field configuration row
|
|
*
|
|
* Inline editing of key, type, label, required, validation, condition.
|
|
* Type-specific validation options appear contextually.
|
|
*/
|
|
function FieldEditor({ field, onChange, onRemove, allFields }) {
|
|
function set(k, v) { onChange({ ...field, [k]: v }); }
|
|
|
|
return (
|
|
<div className="field-editor">
|
|
<div className="form-row">
|
|
<div className="form-group" style={{ flex: 2 }}>
|
|
<label>Label</label>
|
|
<input value={field.label} onChange={e => set('label', e.target.value)} />
|
|
</div>
|
|
<div className="form-group" style={{ flex: 1 }}>
|
|
<label>Key</label>
|
|
<input value={field.key} onChange={e => set('key', e.target.value)}
|
|
placeholder="field_name" />
|
|
</div>
|
|
<div className="form-group" style={{ flex: 1 }}>
|
|
<label>Type</label>
|
|
<select value={field.type} onChange={e => set('type', e.target.value)}>
|
|
<option value="text">Text</option>
|
|
<option value="textarea">Textarea</option>
|
|
<option value="email">Email</option>
|
|
<option value="number">Number</option>
|
|
<option value="date">Date</option>
|
|
<option value="select">Select</option>
|
|
<option value="checkbox">Checkbox</option>
|
|
<option value="file">File</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="toggle-label">
|
|
<input type="checkbox" checked={field.required}
|
|
onChange={e => set('required', e.target.checked)} />
|
|
Required
|
|
</label>
|
|
|
|
{/* Type-specific validation — only render what applies */}
|
|
{(field.type === 'text' || field.type === 'textarea') && (
|
|
<ValidationText validation={field.validation}
|
|
onChange={v => set('validation', v)} />
|
|
)}
|
|
{field.type === 'number' && (
|
|
<ValidationNumber validation={field.validation}
|
|
onChange={v => set('validation', v)} />
|
|
)}
|
|
{field.type === 'select' && (
|
|
<OptionsEditor options={field.options || []}
|
|
onChange={o => set('options', o)} />
|
|
)}
|
|
</div>
|
|
|
|
{/* Conditional visibility */}
|
|
<ConditionEditor condition={field.condition}
|
|
onChange={c => set('condition', c)}
|
|
allFields={allFields} />
|
|
|
|
<button className="btn-small btn-danger" onClick={onRemove}>Remove</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### BE changes
|
|
|
|
None. The form builder is pure FE — it produces the same
|
|
`TypedFormTemplate` JSON that already exists. The BE validation
|
|
in `models/workflow.go` is unchanged.
|
|
|
|
### Deliverables
|
|
|
|
| File | What |
|
|
|------|------|
|
|
| `src/js/sw/components/form-builder/index.js` | FormBuilder root |
|
|
| `src/js/sw/components/form-builder/field-editor.js` | FieldEditor, type-specific validators |
|
|
| `src/js/sw/components/form-builder/fieldset-editor.js` | Fieldset grouping |
|
|
| `src/js/sw/components/form-builder/condition-editor.js` | Conditional visibility |
|
|
| `src/js/sw/components/form-builder/preview.js` | Live form preview |
|
|
| `src/css/form-builder.css` | Styles |
|
|
| Integration into StageEditor (E2 from .15) | "Edit Form" button opens builder |
|
|
|
|
---
|
|
|
|
## v0.38.1 — Stage Reorder + Bulk Ops
|
|
|
|
### Stage drag-and-drop
|
|
|
|
Replace button-based reorder with drag-and-drop in the stage editor.
|
|
No external library — use the HTML5 Drag and Drop API with
|
|
`draggable`, `ondragstart`, `ondragover`, `ondrop`.
|
|
|
|
The reorder PATCH endpoint already exists:
|
|
`PATCH /api/v1/teams/:teamId/workflows/:wfId/stages/reorder`
|
|
|
|
### Bulk assignment operations
|
|
|
|
Team admins managing 20+ assignments need batch actions.
|
|
|
|
```jsx
|
|
/**
|
|
* BulkAssignmentBar — selection-based bulk actions
|
|
*
|
|
* Appears above the queue when 1+ assignments are selected.
|
|
* Actions: bulk cancel, bulk reassign (to specific user).
|
|
*/
|
|
function BulkAssignmentBar({ selected, teamId, onAction }) {
|
|
const [reassignTo, setReassignTo] = useState('');
|
|
|
|
async function bulkCancel() {
|
|
const ok = await sw.confirm(`Cancel ${selected.length} assignments?`, true);
|
|
if (!ok) return;
|
|
await Promise.all(selected.map(id => sw.api.workflowAssignments.cancel(id)));
|
|
sw.toast(`${selected.length} cancelled`, 'success');
|
|
onAction();
|
|
}
|
|
|
|
async function bulkReassign() {
|
|
if (!reassignTo) return;
|
|
await Promise.all(selected.map(id =>
|
|
sw.api.workflowAssignments.reassign(id, reassignTo)
|
|
));
|
|
sw.toast(`${selected.length} reassigned`, 'success');
|
|
onAction();
|
|
}
|
|
|
|
return (
|
|
<div className="bulk-bar">
|
|
<span>{selected.length} selected</span>
|
|
<button className="btn-small btn-danger" onClick={bulkCancel}>Cancel All</button>
|
|
<div className="bulk-bar__reassign">
|
|
<TeamMemberPicker teamId={teamId} value={reassignTo}
|
|
onChange={setReassignTo} />
|
|
<button className="btn-small" onClick={bulkReassign}
|
|
disabled={!reassignTo}>
|
|
Reassign
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### BE changes
|
|
|
|
None. Bulk ops are client-side `Promise.all` over existing single
|
|
endpoints. If performance becomes an issue at scale (50+ assignments),
|
|
add a `POST /api/v1/workflow-assignments/bulk` endpoint in a future
|
|
patch. YAGNI for now.
|
|
|
|
---
|
|
|
|
## v0.38.2 — SLA Alerting
|
|
|
|
The monitor already computes `sla_breached`. This version makes it
|
|
proactive instead of requiring someone to check the dashboard.
|
|
|
|
### Scheduled SLA scanner
|
|
|
|
A new periodic task (Go scheduler, not Starlark) that runs every N
|
|
minutes:
|
|
|
|
1. Query active workflow channels with `sla_seconds` configured
|
|
2. Compute breach status from `stage_entered_at`
|
|
3. For newly breached instances (not yet notified):
|
|
- Create notification for the assigned team members
|
|
- Emit `workflow.sla_breached` WS event
|
|
- If webhook_url configured, fire webhook
|
|
|
|
### Data changes
|
|
|
|
Add `sla_notified_at` (nullable timestamp) to the channels table.
|
|
The scanner only fires notifications when `sla_notified_at IS NULL`
|
|
and the SLA is breached, then sets the timestamp. Prevents duplicate
|
|
alerts.
|
|
|
|
### FE changes
|
|
|
|
The queue items (E1, E4) already render `sla_breached` badges. Add:
|
|
- Pulsing animation on breached badges
|
|
- Toast on `workflow.sla_breached` WS event
|
|
- Notification bell integration (already wired in .15 via
|
|
`notifications.NotifyWorkflowClaimed` pattern)
|
|
|
|
### Deliverables
|
|
|
|
| File | What |
|
|
|------|------|
|
|
| `server/scheduler/sla_scanner.go` | Periodic SLA check |
|
|
| `server/notifications/workflow_sla.go` | SLA notification builder |
|
|
| Migration: `sla_notified_at` column | Both PG + SQLite |
|
|
| FE: badge animation CSS | `src/css/sw-primitives.css` |
|
|
|
|
---
|
|
|
|
## v0.38.3 — Visitor Experience
|
|
|
|
The visitor currently gets a raw workflow surface. After .15 they get
|
|
a terminal screen (E5) when their stages are done. This version makes
|
|
the visitor experience feel like a product.
|
|
|
|
### Scope
|
|
|
|
1. **Branded entry page** — the workflow landing page already supports
|
|
branding (`accent_color`, `logo_url`, `tagline`). Extend with:
|
|
- Custom welcome text (markdown)
|
|
- Background image/gradient
|
|
- "Powered by" toggle (hide/show Switchboard branding)
|
|
|
|
2. **Progress indicator** — visitor sees "Step 1 of N" for their
|
|
visible stages only. Internal team stages are invisible. The
|
|
progress bar counts only stages where the visitor is the audience.
|
|
|
|
3. **Email notifications** — optional. When configured on the workflow:
|
|
- Collect email at stage 0 (or extract from form data)
|
|
- Send "request received" confirmation
|
|
- Send "your request is complete" when workflow completes
|
|
- No notifications for internal stage transitions
|
|
|
|
4. **Session resume** — visitor who closes the browser can return to
|
|
`/w/:id/:slug` and resume where they left off. Already partially
|
|
implemented via `sb_session` cookie. Polish the UX: show "Welcome
|
|
back, pick up where you left off" instead of starting fresh.
|
|
|
|
### BE changes
|
|
|
|
```go
|
|
// Workflow model additions:
|
|
type WorkflowBranding struct {
|
|
AccentColor string `json:"accent_color"`
|
|
LogoURL string `json:"logo_url"`
|
|
Tagline string `json:"tagline"`
|
|
WelcomeText string `json:"welcome_text"` // NEW: markdown
|
|
BackgroundCSS string `json:"background_css"` // NEW: gradient or image URL
|
|
HidePoweredBy bool `json:"hide_powered_by"` // NEW
|
|
}
|
|
|
|
// Email notification config (in workflow settings):
|
|
type WorkflowEmailConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
FromName string `json:"from_name"`
|
|
OnSubmit string `json:"on_submit_template"` // markdown template
|
|
OnComplete string `json:"on_complete_template"` // markdown template
|
|
}
|
|
```
|
|
|
|
Email sending requires an SMTP config at the platform level
|
|
(admin settings). If not configured, email toggle is disabled in
|
|
the workflow editor with a hint.
|
|
|
|
### JSX Exemplar
|
|
|
|
```jsx
|
|
/**
|
|
* VisitorProgress — step indicator for visitor-facing stages
|
|
*
|
|
* Mount: workflow surface, above the stage content
|
|
* Only counts stages where audience = visitor.
|
|
*/
|
|
function VisitorProgress({ stages, currentStage }) {
|
|
// Filter to visitor-facing stages only
|
|
const visitorStages = stages.filter(s => !s.assignment_team_id);
|
|
const visitorIndex = visitorStages.findIndex(s => s.ordinal === currentStage);
|
|
const total = visitorStages.length;
|
|
|
|
if (total <= 1) return null; // Single stage = no progress bar
|
|
|
|
return (
|
|
<div className="visitor-progress">
|
|
{visitorStages.map((s, i) => (
|
|
<div key={s.id}
|
|
className={`visitor-progress__step ${
|
|
i < visitorIndex ? 'done' :
|
|
i === visitorIndex ? 'active' : ''
|
|
}`}>
|
|
<div className="visitor-progress__dot">{i < visitorIndex ? '✓' : i + 1}</div>
|
|
<span className="visitor-progress__label">{s.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## v0.38.4 — Workflow Templates + Clone
|
|
|
|
Team admins shouldn't rebuild common patterns from scratch. Two
|
|
features:
|
|
|
|
### Clone workflow
|
|
|
|
"Duplicate" button on the workflow editor. Creates a new workflow with
|
|
the same stages, form templates, and routing rules. New slug, new name
|
|
(appended "- Copy"). No versions carried over (must re-publish).
|
|
|
|
```
|
|
POST /api/v1/teams/:teamId/workflows/:id/clone
|
|
→ 201 { ...newWorkflow }
|
|
```
|
|
|
|
Pure BE operation — deep-copies workflow + stages, generates new IDs.
|
|
|
|
### Built-in templates
|
|
|
|
Seed a `workflow_templates` table with 3-5 common patterns:
|
|
|
|
| Template | Stages | Description |
|
|
|----------|--------|-------------|
|
|
| Customer Intake | form → review | Public form, team reviews |
|
|
| IT Help Desk | LLM chat → triage → resolution | AI-assisted intake |
|
|
| Approval Chain | form → approve → approve → notify | Multi-level approval |
|
|
| Feedback Survey | form (multi-step) → analytics | Data collection |
|
|
| Onboarding | form → team A → team B → team A | Multi-team handoff |
|
|
|
|
"New from template" in the team-admin workflow creation flow.
|
|
Templates are read-only platform data, not user-editable. They
|
|
serve as starting points — clone and customize.
|
|
|
|
### FE changes
|
|
|
|
- "Duplicate" button in workflow editor header
|
|
- "New from Template" option in workflow creation flow
|
|
- Template picker modal with descriptions
|
|
|
|
---
|
|
|
|
## v0.38.5 — Analytics Dashboard
|
|
|
|
Team leads need to answer: how long does each stage take, where are
|
|
we bottlenecked, what's our SLA compliance rate.
|
|
|
|
### Data source
|
|
|
|
No new data collection. Everything is derivable from existing columns:
|
|
|
|
- `channels.created_at` — instance start time
|
|
- `channels.stage_entered_at` — current stage entry
|
|
- `channels.workflow_status` — terminal state
|
|
- `workflow_assignments.created_at / claimed_at / completed_at` — assignment lifecycle timestamps
|
|
|
|
### Metrics
|
|
|
|
| Metric | Derivation |
|
|
|--------|------------|
|
|
| Throughput | Count of `completed` instances per time period |
|
|
| Average cycle time | `completed_at - created_at` across instances |
|
|
| Stage dwell time | `next_stage_entered_at - stage_entered_at` (requires computing from history) |
|
|
| SLA compliance | `breached / total` per stage with SLA configured |
|
|
| Assignment claim time | `claimed_at - created_at` on assignments |
|
|
| Queue depth over time | Snapshot of unassigned count (requires periodic sampling or derive from events) |
|
|
|
|
### JSX Exemplar
|
|
|
|
```jsx
|
|
/**
|
|
* WorkflowAnalytics — team-admin analytics tab
|
|
*
|
|
* Mount: team-admin workflows surface, "Analytics" tab
|
|
* Data: GET /api/v1/teams/:teamId/workflows/analytics
|
|
* ?workflow_id=...&period=7d|30d|90d
|
|
*/
|
|
function WorkflowAnalytics({ teamId }) {
|
|
const [data, setData] = useState(null);
|
|
const [workflowId, setWorkflowId] = useState('');
|
|
const [period, setPeriod] = useState('30d');
|
|
|
|
// ... load, filter controls ...
|
|
|
|
return (
|
|
<div className="wf-analytics">
|
|
<div className="wf-analytics__filters">
|
|
<WorkflowPicker teamId={teamId} value={workflowId} onChange={setWorkflowId} />
|
|
<PeriodPicker value={period} onChange={setPeriod} />
|
|
</div>
|
|
|
|
<div className="wf-analytics__cards">
|
|
<StatCard label="Completed" value={data.completed_count} />
|
|
<StatCard label="Avg Cycle Time" value={formatDuration(data.avg_cycle_seconds)} />
|
|
<StatCard label="SLA Compliance" value={`${data.sla_compliance_pct}%`}
|
|
variant={data.sla_compliance_pct < 80 ? 'danger' : 'success'} />
|
|
<StatCard label="Avg Claim Time" value={formatDuration(data.avg_claim_seconds)} />
|
|
</div>
|
|
|
|
{/* Stage funnel with dwell times — uses existing funnel endpoint + dwell data */}
|
|
<StageFunnel stages={data.stage_metrics} />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### BE changes
|
|
|
|
New endpoint:
|
|
```
|
|
GET /api/v1/teams/:teamId/workflows/analytics
|
|
?workflow_id=...&period=7d|30d|90d
|
|
```
|
|
|
|
Aggregation query over channels + assignments tables. No new tables.
|
|
Consider materialized views or periodic snapshot if query cost is
|
|
too high on large datasets (unlikely at 50-user scale).
|
|
|
|
---
|
|
|
|
## Changeset Budget
|
|
|
|
| Version | Estimated CS | Heaviest piece |
|
|
|---------|-------------|----------------|
|
|
| v0.38.0 | 4 | Form builder component tree (5+ files) |
|
|
| v0.38.1 | 2 | Drag-and-drop is fiddly but small |
|
|
| v0.38.2 | 3 | SLA scanner + notification plumbing |
|
|
| v0.38.3 | 4 | Visitor branding + email + progress |
|
|
| v0.38.4 | 2 | Clone is one BE endpoint + FE button |
|
|
| v0.38.5 | 3 | Analytics query + chart components |
|
|
| **Total** | **~18** | |
|
|
|
|
---
|
|
|
|
## Risk Notes
|
|
|
|
**v0.38.0 (Form Builder) is the long pole.** If this slips, the MVP
|
|
gate phrase "build workflows visually" fails. Prioritize this over
|
|
everything else in the series. If time is tight, ship a simplified
|
|
version (no fieldsets, no conditional visibility) and add those in a
|
|
patch.
|
|
|
|
**v0.38.3 (Email) depends on platform SMTP.** If SMTP isn't
|
|
configured, the feature is invisible. Consider making the email
|
|
config a prerequisite admin step with a setup wizard, or defer email
|
|
to v0.38.5 and keep .3 focused on branding + progress.
|
|
|
|
**v0.38.5 (Analytics) can be cut.** If the series is running long,
|
|
analytics is the most deferrable item — it's nice-to-have, not
|
|
gate-blocking. The existing monitor + funnel endpoints cover the
|
|
critical "is anything stuck" question.
|