From 9f44354901360c32be779747b4b436b421e2f9f7 Mon Sep 17 00:00:00 2001 From: mackeh Date: Sat, 14 Feb 2026 11:20:31 +0100 Subject: [PATCH] feat(onboarding): add template setup wizard, walkthrough, and activity roadmap --- .github/workflows/ci.yml | 4 + CHANGELOG.md | 6 + README.md | 6 +- apps/server/package.json | 1 + apps/server/src/index.ts | 14 + apps/server/src/lib/activities.test.ts | 9 +- apps/server/src/lib/activities.ts | 279 ++++++++++++++++--- apps/server/src/lib/templateQuality.test.ts | 9 + apps/server/src/lib/templateQuality.ts | 85 ++++++ apps/server/src/lib/templates.test.ts | 3 + apps/server/src/lib/templates.ts | 288 ++++++++++++++++++- apps/web/e2e/mockApi.ts | 33 ++- apps/web/src/App.tsx | 289 +++++++++++++++++++- apps/web/src/api.ts | 10 +- apps/web/src/lib/starterWalkthrough.test.ts | 14 + apps/web/src/lib/starterWalkthrough.ts | 44 +++ apps/web/src/lib/templateSetup.test.ts | 52 ++++ apps/web/src/lib/templateSetup.ts | 66 +++++ apps/web/src/styles.css | 17 ++ apps/web/src/types.ts | 60 ++++ docs/ACTIVITY_PACK_ROADMAP.md | 41 +++ docs/API_REFERENCE.md | 6 + docs/CONTRIBUTING.md | 3 + docs/DEMOS.md | 17 +- docs/ONBOARDING.md | 4 + docs/README.md | 1 + 26 files changed, 1302 insertions(+), 59 deletions(-) create mode 100644 apps/server/src/lib/templateQuality.test.ts create mode 100644 apps/server/src/lib/templateQuality.ts create mode 100644 apps/web/src/lib/starterWalkthrough.test.ts create mode 100644 apps/web/src/lib/starterWalkthrough.ts create mode 100644 apps/web/src/lib/templateSetup.test.ts create mode 100644 apps/web/src/lib/templateSetup.ts create mode 100644 docs/ACTIVITY_PACK_ROADMAP.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b509a4..e31c73b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,10 @@ jobs: working-directory: apps/server run: npm test + - name: Template quality gate + working-directory: apps/server + run: npm run test:templates + - name: Build working-directory: apps/server run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5550d..58f9419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,15 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht - Autopilot plan diagnostics: overall confidence score, node-level insights, and fallback template options. - Contributor onboarding package: 10-minute tutorial, starter workflow file, and reusable docs templates. - Real-world starter template pack (invoice approval, web scrape sync, CSV cleanup, email triage, health check alert). +- Template setup metadata and `GET /api/templates/:templateId` endpoint for wizard-driven setup. +- Template setup wizard in UI with required fields, connection checks, sample-input copy, and preflight-based readiness. +- Starter walkthrough panel in UI for first workflow onboarding. +- Activity pack roadmap model (`pack`, `phase`) with phase-focus summary in `/api/activities`. +- Activity roadmap doc: `docs/ACTIVITY_PACK_ROADMAP.md`. ### Changed - CI now includes browser smoke validation (`Web E2E Smoke`). +- CI server job now runs an explicit template quality gate (`npm run test:templates`). - Web editor keyboard shortcuts now include undo/redo and selection-aware delete behavior. - Web recorder now follows capture -> review -> insert flow instead of immediate node injection. - Autopilot now requires explicit confirm-before-create flow and uses richer starter templates for vague prompts. diff --git a/README.md b/README.md index 7a1f164..697fb7e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ It combines a drag-and-drop workflow studio, resilient execution, AI-assisted au - Web automation (Playwright) and desktop automation (agent service) - Recorder flows for web and desktop action capture with review-before-insert draft editing - Autopilot workflow generation from natural-language prompts with confidence scoring and confirm-before-create review +- Template setup wizard with required fields, preflight checks, sample input copy, and run-readiness status +- In-app starter walkthrough for first workflow creation and publish path - AI nodes: `transform_llm`, `document_understanding`, `clipboard_ai_transfer` - Integrations (`http_api`, `postgresql`, `mysql`, `mongodb`, `google_sheets`, `airtable`, `s3`) - Orchestrator queue with attended/unattended robots and dispatch lifecycle @@ -68,7 +70,8 @@ Use built-in templates as a fast path from idea to first successful run: Create from UI: 1. Open `Templates` in the left sidebar. 2. Select a starter template. -3. Click `Create Workflow`. +3. Complete `Template Setup Wizard` fields and click `Validate Setup`. +4. Click `Create Workflow`. ## Contributor Onboarding New contributors should start here: @@ -145,6 +148,7 @@ Tag pushes (`v*.*.*`) trigger `.github/workflows/release.yml`, which reruns vali - `docs/ONBOARDING.md` - `docs/tutorials/FIRST_AUTOMATION_10_MIN.md` - `docs/examples/workflows/first-automation.workflow.json` +- `docs/ACTIVITY_PACK_ROADMAP.md` - `docs/ARCHITECTURE.md` - `docs/API_REFERENCE.md` - `docs/DEPLOYMENT.md` diff --git a/apps/server/package.json b/apps/server/package.json index cc006a4..e9378de 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -7,6 +7,7 @@ "start": "node dist/index.js", "build": "tsc -p tsconfig.json", "test": "node --import tsx --test src/**/*.test.ts", + "test:templates": "node --import tsx --test src/lib/templates.test.ts src/lib/templateQuality.test.ts", "test:critical": "node --import tsx --test src/lib/execution-flows.test.ts src/lib/runDiff.test.ts src/lib/recorder.test.ts src/lib/agent.test.ts", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev" diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index e2a50c1..b4f63c6 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -545,6 +545,20 @@ app.get("/api/templates", canReadTemplates, async (_req, res) => { res.json(templates); }); +app.get("/api/templates/:templateId", canReadTemplates, async (req, res) => { + const templateId = String(req.params.templateId || "").trim(); + if (!templateId) { + res.status(400).json({ error: "Template id is required" }); + return; + } + const template = getWorkflowTemplate(templateId); + if (!template) { + res.status(404).json({ error: "Template not found" }); + return; + } + res.json(template); +}); + app.get("/api/activities", canReadActivities, async (_req, res) => { const catalog = await cache.getOrSet("activities:catalog", CACHE_TTL.templatesMs, async () => listActivities()); res.json(catalog); diff --git a/apps/server/src/lib/activities.test.ts b/apps/server/src/lib/activities.test.ts index 4cbdffb..5917f6f 100644 --- a/apps/server/src/lib/activities.test.ts +++ b/apps/server/src/lib/activities.test.ts @@ -5,8 +5,15 @@ import { listActivities } from "./activities.js"; test("listActivities exposes summary counts and target size", () => { const catalog = listActivities(); assert.equal(catalog.targetLibrarySize, 300); - assert.ok(catalog.currentTotal >= 30); + assert.ok(catalog.currentTotal >= 90); assert.ok(catalog.availableCount > 0); assert.ok(catalog.plannedCount > 0); assert.ok(catalog.byCategory.Core >= 1); + assert.ok(catalog.byPhase["phase-1"] > 0); + assert.ok(catalog.byPhase["phase-2"] > 0); + assert.ok(catalog.byPack["system-core"] > 0); + assert.ok(Array.isArray(catalog.roadmap)); + assert.ok(catalog.roadmap.some((pack) => pack.id === "system-core")); + assert.ok(catalog.roadmap.some((pack) => pack.id === "integration-service")); + assert.ok(catalog.roadmap.some((pack) => pack.id === "ai-center")); }); diff --git a/apps/server/src/lib/activities.ts b/apps/server/src/lib/activities.ts index 0b20ad2..bf1b1c4 100644 --- a/apps/server/src/lib/activities.ts +++ b/apps/server/src/lib/activities.ts @@ -1,58 +1,207 @@ +export type ActivityPhase = "phase-1" | "phase-2" | "phase-3"; + export type ActivityDescriptor = { id: string; label: string; category: string; + pack: string; + phase: ActivityPhase; status: "available" | "planned"; description: string; aliases?: string[]; }; const activities: ActivityDescriptor[] = [ - { id: "http_request", label: "HTTP Request", category: "Core", status: "available", description: "Call REST endpoints." }, - { id: "set_variable", label: "Set Variable", category: "Core", status: "available", description: "Set workflow context values." }, - { id: "transform_llm", label: "LLM Transform", category: "AI", status: "available", description: "Use local LLM to transform content." }, - { id: "validate_record", label: "Validate Record", category: "Core", status: "available", description: "Validate data against schema rules." }, - { id: "submit_guard", label: "Submit Guard", category: "Core", status: "available", description: "Gate outputs through validation." }, - { id: "manual_approval", label: "Manual Approval", category: "Control", status: "available", description: "Pause run for human approval." }, - { id: "conditional_branch", label: "Conditional Branch", category: "Control", status: "available", description: "Route based on conditions." }, - { id: "loop_iterate", label: "Loop Iterate", category: "Control", status: "available", description: "Iterate over array values." }, - { id: "parallel_execute", label: "Parallel Execute", category: "Control", status: "available", description: "Execute sub-tasks concurrently." }, - { id: "data_import_csv", label: "CSV Import", category: "Data", status: "available", description: "Import CSV rows into context." }, - { id: "integration_request", label: "Integration Request", category: "Data", status: "available", description: "Call configured integration connectors." }, - { id: "playwright_navigate", label: "Web Navigate", category: "Web", status: "available", description: "Navigate browser to URL." }, - { id: "playwright_click", label: "Web Click", category: "Web", status: "available", description: "Click web elements via selector." }, - { id: "playwright_fill", label: "Web Fill", category: "Web", status: "available", description: "Fill web form fields." }, - { id: "playwright_extract", label: "Web Extract", category: "Web", status: "available", description: "Extract text/data from web pages." }, - { id: "playwright_visual_assert", label: "Web Visual Assert", category: "Web", status: "available", description: "Run visual regression assertions." }, - { id: "desktop_click", label: "Desktop Click", category: "Desktop", status: "available", description: "Click desktop coordinates." }, - { id: "desktop_click_image", label: "Desktop Click Image", category: "Desktop", status: "available", description: "Image-based desktop click." }, - { id: "desktop_type", label: "Desktop Type", category: "Desktop", status: "available", description: "Type text into desktop apps." }, - { id: "desktop_wait_for_image", label: "Desktop Wait Image", category: "Desktop", status: "available", description: "Wait for image on screen." }, - - { id: "excel_read_range", label: "Excel Read Range", category: "Office", status: "planned", description: "Read tabular ranges from Excel." }, - { id: "excel_write_range", label: "Excel Write Range", category: "Office", status: "planned", description: "Write data to Excel sheets." }, - { id: "excel_filter_table", label: "Excel Filter Table", category: "Office", status: "planned", description: "Filter workbook data tables." }, - { id: "pdf_extract_text", label: "PDF Extract Text", category: "Documents", status: "planned", description: "Extract text from PDFs." }, - { id: "pdf_split_merge", label: "PDF Split Merge", category: "Documents", status: "planned", description: "Split and merge PDF files." }, - { id: "email_send", label: "Email Send", category: "Communication", status: "planned", description: "Send emails with attachments." }, - { id: "email_read_inbox", label: "Email Read Inbox", category: "Communication", status: "planned", description: "Read and parse inbox messages." }, - { id: "clipboard_ai_transfer", label: "Clipboard AI Transfer", category: "AI", status: "available", description: "Context-aware copy/paste normalization between workflow contexts." }, - { id: "document_understanding", label: "Document Understanding", category: "AI", status: "available", description: "Extract structured fields from raw document text." }, - { id: "sap_table_extract", label: "SAP Table Extract", category: "Enterprise Apps", status: "planned", description: "Extract structured SAP data." }, - { id: "java_ui_extract", label: "Java UI Extract", category: "Enterprise Apps", status: "planned", description: "Extract data from Java applications." }, - { id: "dotnet_ui_extract", label: ".NET UI Extract", category: "Enterprise Apps", status: "planned", description: "Extract data from .NET applications." }, - { id: "task_capture_record", label: "Task Capture", category: "Discovery", status: "planned", description: "Document process maps from actions." }, - { id: "task_mining", label: "Task Mining", category: "Discovery", status: "planned", description: "Mine repetitive UI activity patterns." }, - { id: "process_mining", label: "Process Mining", category: "Discovery", status: "planned", description: "Analyze bottlenecks from event logs." }, - { id: "robot_attended_trigger", label: "Attended Robot Trigger", category: "Execution", status: "planned", description: "Human-triggered robot actions." }, - { id: "robot_unattended_queue", label: "Unattended Robot Queue", category: "Execution", status: "planned", description: "Queue autonomous robot jobs." }, - { id: "ai_center_model_infer", label: "AI Center Model Infer", category: "AI", status: "planned", description: "Use hosted ML model inference." }, - { id: "orchestrator_queue", label: "Orchestrator Queue", category: "Management", status: "available", description: "Manage queue-based workloads." }, - { id: "orchestrator_asset", label: "Orchestrator Asset", category: "Management", status: "planned", description: "Manage centralized automation assets." }, - { id: "app_builder_form", label: "Apps Form", category: "Apps", status: "planned", description: "Low-code business app form component." }, - { id: "app_builder_dashboard", label: "Apps Dashboard", category: "Apps", status: "planned", description: "Low-code KPI dashboard component." } + { id: "http_request", label: "HTTP Request", category: "Core", pack: "system-core", phase: "phase-1", status: "available", description: "Call REST endpoints." }, + { id: "set_variable", label: "Assign", category: "Core", pack: "system-core", phase: "phase-1", status: "available", description: "Set workflow context values.", aliases: ["workflow_assign"] }, + { id: "workflow_delay", label: "Delay", category: "Core", pack: "system-core", phase: "phase-1", status: "planned", description: "Pause execution for a duration." }, + { id: "do_while", label: "Do While", category: "Core", pack: "system-core", phase: "phase-1", status: "planned", description: "Loop while condition is true." }, + { id: "conditional_branch", label: "If", category: "Control", pack: "system-core", phase: "phase-1", status: "available", description: "Route execution based on a condition.", aliases: ["workflow_if"] }, + { id: "workflow_switch", label: "Switch", category: "Control", pack: "system-core", phase: "phase-1", status: "planned", description: "Route to one of many branches." }, + { id: "parallel_execute", label: "Parallel", category: "Control", pack: "system-core", phase: "phase-1", status: "available", description: "Execute sub-tasks concurrently.", aliases: ["workflow_parallel"] }, + { id: "loop_iterate", label: "For Each", category: "Control", pack: "system-core", phase: "phase-1", status: "available", description: "Iterate over collection values.", aliases: ["workflow_for_each"] }, + { id: "retry_scope", label: "Retry Scope", category: "Control", pack: "system-core", phase: "phase-1", status: "planned", description: "Retry enclosed steps with policy controls." }, + { id: "trigger_scope", label: "Trigger Scope", category: "Control", pack: "system-core", phase: "phase-1", status: "planned", description: "Execute workflow branch from trigger context." }, + { id: "manual_approval", label: "Manual Approval", category: "Control", pack: "system-core", phase: "phase-1", status: "available", description: "Pause run for human approval." }, + { id: "submit_guard", label: "Submit Guard", category: "Core", pack: "system-core", phase: "phase-1", status: "available", description: "Gate outputs through validation." }, + { id: "validate_record", label: "Validate Record", category: "Core", pack: "system-core", phase: "phase-1", status: "available", description: "Validate data against schema rules." }, + + { id: "build_data_table", label: "Build Data Table", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Create a structured in-memory table." }, + { id: "filter_data_table", label: "Filter Data Table", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Filter rows from a data table." }, + { id: "sort_data_table", label: "Sort Data Table", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Sort rows by selected columns." }, + { id: "add_data_row", label: "Add Data Row", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Append row values to a data table." }, + { id: "add_data_column", label: "Add Data Column", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Append new column definitions." }, + { id: "output_data_table", label: "Output Data Table", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Export table to downstream steps." }, + { id: "lookup_data_table", label: "Lookup Data Table", category: "Data Table", pack: "data-table", phase: "phase-1", status: "planned", description: "Lookup row values by key." }, + { id: "data_import_csv", label: "CSV Import", category: "Data", pack: "data-table", phase: "phase-1", status: "available", description: "Import CSV rows into context." }, + + { id: "copy_file", label: "Copy File", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Copy files between locations." }, + { id: "copy_folder", label: "Copy Folder", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Copy folder trees." }, + { id: "create_file", label: "Create File", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Create new file resources." }, + { id: "create_folder", label: "Create Folder", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Create new directories." }, + { id: "file_delete", label: "Delete File/Folder", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Delete files or folders." }, + { id: "move_file", label: "Move", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Move files/folders to new paths." }, + { id: "rename_file", label: "Rename", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Rename file or folder targets." }, + { id: "append_line", label: "Append Line", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Append text lines to file." }, + { id: "path_exists", label: "Path Exists", category: "File & Folder", pack: "file-folder", phase: "phase-1", status: "planned", description: "Check path existence." }, + + { id: "use_application_browser", label: "Use Application/Browser", category: "Interaction", pack: "interaction", phase: "phase-1", status: "planned", description: "Scope UI automation context." }, + { id: "playwright_click", label: "Click", category: "Interaction", pack: "interaction", phase: "phase-1", status: "available", description: "Click web elements via selector." }, + { id: "playwright_fill", label: "Type Into", category: "Interaction", pack: "interaction", phase: "phase-1", status: "available", description: "Fill web form fields." }, + { id: "hover", label: "Hover", category: "Interaction", pack: "interaction", phase: "phase-1", status: "planned", description: "Hover over UI targets." }, + { id: "check_app_state", label: "Check App State", category: "Interaction", pack: "interaction", phase: "phase-1", status: "planned", description: "Assert application state." }, + { id: "playwright_extract", label: "Get Text", category: "Interaction", pack: "interaction", phase: "phase-1", status: "available", description: "Extract text/data from app surfaces." }, + { id: "extract_table_data", label: "Extract Table Data", category: "Interaction", pack: "interaction", phase: "phase-1", status: "planned", description: "Extract tabular UI data." }, + { id: "playwright_navigate", label: "Navigate", category: "Web", pack: "interaction", phase: "phase-1", status: "available", description: "Navigate browser to URL." }, + + { id: "get_clipboard", label: "Get Clipboard", category: "Input/Output", pack: "io", phase: "phase-1", status: "planned", description: "Read current clipboard value." }, + { id: "set_clipboard", label: "Set Clipboard", category: "Input/Output", pack: "io", phase: "phase-1", status: "planned", description: "Write value into clipboard." }, + { id: "keyboard_shortcuts", label: "Keyboard Shortcuts", category: "Input/Output", pack: "io", phase: "phase-1", status: "planned", description: "Send shortcut key combinations." }, + { id: "take_screenshot", label: "Take Screenshot", category: "Input/Output", pack: "io", phase: "phase-1", status: "planned", description: "Capture screenshot artifacts." }, + + { id: "tap", label: "Tap", category: "Mobile", pack: "mobile", phase: "phase-2", status: "planned", description: "Tap mobile UI target." }, + { id: "swipe", label: "Swipe", category: "Mobile", pack: "mobile", phase: "phase-2", status: "planned", description: "Swipe mobile screen gesture." }, + { id: "mobile_type_text", label: "Type Text", category: "Mobile", pack: "mobile", phase: "phase-2", status: "planned", description: "Type text on mobile device." }, + { id: "set_device_orientation", label: "Set Device Orientation", category: "Mobile", pack: "mobile", phase: "phase-2", status: "planned", description: "Rotate mobile device orientation." }, + + { id: "get_asset", label: "Get Asset", category: "Orchestrator", pack: "orchestrator", phase: "phase-1", status: "planned", description: "Read orchestrator asset value." }, + { id: "set_asset", label: "Set Asset", category: "Orchestrator", pack: "orchestrator", phase: "phase-1", status: "planned", description: "Write orchestrator asset value." }, + { id: "add_queue_item", label: "Add Queue Item", category: "Orchestrator", pack: "orchestrator", phase: "phase-1", status: "planned", description: "Queue transactional work item." }, + { id: "get_transaction_item", label: "Get Transaction Item", category: "Orchestrator", pack: "orchestrator", phase: "phase-1", status: "planned", description: "Pull next transaction item." }, + { id: "set_transaction_status", label: "Set Transaction Status", category: "Orchestrator", pack: "orchestrator", phase: "phase-1", status: "planned", description: "Complete/fail a transaction item." }, + { id: "orchestrator_queue", label: "Orchestrator Queue", category: "Management", pack: "orchestrator", phase: "phase-1", status: "available", description: "Manage queue-based workloads." }, + { id: "robot_attended_trigger", label: "Attended Robot Trigger", category: "Execution", pack: "orchestrator", phase: "phase-2", status: "planned", description: "Human-triggered robot actions." }, + { id: "robot_unattended_queue", label: "Unattended Robot Queue", category: "Execution", pack: "orchestrator", phase: "phase-2", status: "planned", description: "Queue autonomous robot jobs." }, + + { id: "excel_process_scope", label: "Excel Process Scope", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Open and manage Excel process context." }, + { id: "use_excel_file", label: "Use Excel File", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Select workbook file scope." }, + { id: "excel_read_range", label: "Read Range", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Read tabular ranges from Excel." }, + { id: "excel_write_range", label: "Write Range", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Write data to Excel sheets." }, + { id: "excel_append_range", label: "Append Range", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Append rows to existing sheets." }, + { id: "excel_pivot_table", label: "Pivot Table", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Create pivot tables from source data." }, + { id: "excel_vlookup", label: "VLookup", category: "Excel", pack: "office", phase: "phase-2", status: "planned", description: "Execute spreadsheet lookup operations." }, + + { id: "send_smtp_mail", label: "Send SMTP Mail Message", category: "Mail", pack: "communications", phase: "phase-2", status: "planned", description: "Send outbound SMTP emails." }, + { id: "get_imap_messages", label: "Get IMAP Mail Messages", category: "Mail", pack: "communications", phase: "phase-2", status: "planned", description: "Fetch inbound IMAP email messages." }, + { id: "forward_reply_email", label: "Forward/Reply Email", category: "Mail", pack: "communications", phase: "phase-2", status: "planned", description: "Forward or reply to messages." }, + { id: "save_attachments", label: "Save Attachments", category: "Mail", pack: "communications", phase: "phase-2", status: "planned", description: "Save attachments to local storage." }, + + { id: "read_pdf_text", label: "Read PDF Text", category: "PDF", pack: "documents", phase: "phase-2", status: "planned", description: "Extract text from PDF files." }, + { id: "read_pdf_ocr", label: "Read PDF with OCR", category: "PDF", pack: "documents", phase: "phase-2", status: "planned", description: "Use OCR for scanned PDFs." }, + { id: "extract_pdf_range", label: "Extract PDF Page Range", category: "PDF", pack: "documents", phase: "phase-2", status: "planned", description: "Split pages from PDF files." }, + { id: "join_pdf_files", label: "Join PDF Files", category: "PDF", pack: "documents", phase: "phase-2", status: "planned", description: "Merge multiple PDFs." }, + + { id: "word_application_scope", label: "Word Application Scope", category: "Word", pack: "documents", phase: "phase-2", status: "planned", description: "Open Word document automation context." }, + { id: "replace_picture", label: "Replace Picture", category: "Word", pack: "documents", phase: "phase-2", status: "planned", description: "Replace embedded images in Word docs." }, + { id: "add_hyperlink", label: "Add Hyperlink", category: "Word", pack: "documents", phase: "phase-2", status: "planned", description: "Insert hyperlinks in documents." }, + { id: "save_as_pdf", label: "Save Document as PDF", category: "Word", pack: "documents", phase: "phase-2", status: "planned", description: "Export Word docs to PDF." }, + + { id: "integration_servicenow", label: "ServiceNow Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to ServiceNow APIs/workflows." }, + { id: "integration_jira", label: "Jira Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to Jira projects/issues." }, + { id: "integration_salesforce", label: "Salesforce Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to Salesforce objects/data." }, + { id: "integration_slack", label: "Slack Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to Slack channels/actions." }, + { id: "integration_microsoft_365", label: "Microsoft 365 Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to Microsoft 365 services." }, + { id: "integration_google_workspace", label: "Google Workspace Connector", category: "Integration Service", pack: "integration-service", phase: "phase-2", status: "planned", description: "Connect to Google Workspace APIs." }, + { id: "integration_request", label: "Integration Request", category: "Data", pack: "integration-service", phase: "phase-1", status: "available", description: "Call configured integration connectors." }, + + { id: "load_taxonomy", label: "Load Taxonomy", category: "Document Understanding", pack: "document-intelligence", phase: "phase-2", status: "planned", description: "Load document taxonomy models." }, + { id: "digitize_document", label: "Digitize Document", category: "Document Understanding", pack: "document-intelligence", phase: "phase-2", status: "planned", description: "Digitize scanned documents." }, + { id: "classify_document_scope", label: "Classify Document Scope", category: "Document Understanding", pack: "document-intelligence", phase: "phase-2", status: "planned", description: "Classify incoming document types." }, + { id: "data_extraction_scope", label: "Data Extraction Scope", category: "Document Understanding", pack: "document-intelligence", phase: "phase-2", status: "planned", description: "Extract structured document fields." }, + { id: "present_validation_station", label: "Present Validation Station", category: "Document Understanding", pack: "document-intelligence", phase: "phase-2", status: "planned", description: "Route extracted data for human validation." }, + { id: "document_understanding", label: "Document Understanding", category: "AI", pack: "document-intelligence", phase: "phase-1", status: "available", description: "Extract structured fields from raw document text." }, + + { id: "summarize_text", label: "Summarize Text", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Summarize long-form text." }, + { id: "translate_text", label: "Translate", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Translate text between languages." }, + { id: "pii_filtering", label: "PII Filtering", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Detect and redact sensitive PII." }, + { id: "categorize_text", label: "Categorize", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Categorize content using AI." }, + { id: "sentiment_analysis", label: "Sentiment Analysis", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Detect sentiment polarity." }, + { id: "image_analysis", label: "Image Analysis", category: "AI Center/GenAI", pack: "ai-center", phase: "phase-3", status: "planned", description: "Analyze images and visual content." }, + { id: "ai_center_model_infer", label: "AI Center Model Infer", category: "AI", pack: "ai-center", phase: "phase-2", status: "planned", description: "Use hosted ML model inference." }, + { id: "transform_llm", label: "LLM Transform", category: "AI", pack: "ai-center", phase: "phase-1", status: "available", description: "Use local LLM to transform content." }, + { id: "clipboard_ai_transfer", label: "Clipboard AI Transfer", category: "AI", pack: "ai-center", phase: "phase-1", status: "available", description: "Context-aware copy/paste normalization between workflow contexts." }, + + { id: "cv_click", label: "CV Click", category: "Computer Vision", pack: "computer-vision", phase: "phase-2", status: "planned", description: "Image-based click interaction." }, + { id: "cv_type_into", label: "CV Type Into", category: "Computer Vision", pack: "computer-vision", phase: "phase-2", status: "planned", description: "Image-anchored typing interaction." }, + { id: "cv_get_text", label: "CV Get Text", category: "Computer Vision", pack: "computer-vision", phase: "phase-2", status: "planned", description: "OCR text extraction from screen regions." }, + { id: "desktop_click", label: "Desktop Click", category: "Desktop", pack: "computer-vision", phase: "phase-1", status: "available", description: "Click desktop coordinates." }, + { id: "desktop_click_image", label: "Desktop Click Image", category: "Desktop", pack: "computer-vision", phase: "phase-1", status: "available", description: "Image-based desktop click." }, + { id: "desktop_type", label: "Desktop Type", category: "Desktop", pack: "computer-vision", phase: "phase-1", status: "available", description: "Type text into desktop apps." }, + { id: "desktop_wait_for_image", label: "Desktop Wait Image", category: "Desktop", pack: "computer-vision", phase: "phase-1", status: "available", description: "Wait for image on screen." }, + + { id: "db_connect", label: "Connect", category: "Database", pack: "database", phase: "phase-2", status: "planned", description: "Open database connection." }, + { id: "db_disconnect", label: "Disconnect", category: "Database", pack: "database", phase: "phase-2", status: "planned", description: "Close database connection." }, + { id: "db_run_query", label: "Run Query", category: "Database", pack: "database", phase: "phase-2", status: "planned", description: "Execute SELECT-style query." }, + { id: "db_run_command", label: "Run Command", category: "Database", pack: "database", phase: "phase-2", status: "planned", description: "Execute non-query command." }, + + { id: "ftp_scope", label: "FTP Scope", category: "FTP", pack: "ftp", phase: "phase-2", status: "planned", description: "Open FTP/SFTP session context." }, + { id: "ftp_download", label: "Download Files", category: "FTP", pack: "ftp", phase: "phase-2", status: "planned", description: "Download files from FTP server." }, + { id: "ftp_upload", label: "Upload Files", category: "FTP", pack: "ftp", phase: "phase-2", status: "planned", description: "Upload files to FTP server." }, + { id: "ftp_delete", label: "Delete", category: "FTP", pack: "ftp", phase: "phase-2", status: "planned", description: "Delete files on FTP server." }, + { id: "ftp_file_exists", label: "File Exists", category: "FTP", pack: "ftp", phase: "phase-2", status: "planned", description: "Check remote file existence." }, + + { id: "terminal_session", label: "Terminal Session", category: "Terminal/Mainframe", pack: "terminal-mainframe", phase: "phase-3", status: "planned", description: "Open terminal or mainframe session." }, + { id: "terminal_get_field", label: "Get Field", category: "Terminal/Mainframe", pack: "terminal-mainframe", phase: "phase-3", status: "planned", description: "Read field value from terminal screen." }, + { id: "terminal_send_keys", label: "Send Keys", category: "Terminal/Mainframe", pack: "terminal-mainframe", phase: "phase-3", status: "planned", description: "Send key sequences to terminal." }, + + { id: "encrypt_file", label: "Encrypt File", category: "Cryptography", pack: "cryptography", phase: "phase-3", status: "planned", description: "Encrypt files using managed keys." }, + { id: "decrypt_file", label: "Decrypt File", category: "Cryptography", pack: "cryptography", phase: "phase-3", status: "planned", description: "Decrypt files using managed keys." }, + + { id: "task_capture_record", label: "Task Capture", category: "Discovery", pack: "discovery", phase: "phase-2", status: "planned", description: "Document process maps from actions." }, + { id: "task_mining", label: "Task Mining", category: "Discovery", pack: "discovery", phase: "phase-2", status: "planned", description: "Mine repetitive UI activity patterns." }, + { id: "process_mining", label: "Process Mining", category: "Discovery", pack: "discovery", phase: "phase-2", status: "planned", description: "Analyze bottlenecks from event logs." }, + + { id: "orchestrator_asset", label: "Orchestrator Asset", category: "Management", pack: "orchestrator", phase: "phase-2", status: "planned", description: "Manage centralized automation assets." }, + { id: "app_builder_form", label: "Apps Form", category: "Apps", pack: "apps", phase: "phase-3", status: "planned", description: "Low-code business app form component." }, + { id: "app_builder_dashboard", label: "Apps Dashboard", category: "Apps", pack: "apps", phase: "phase-3", status: "planned", description: "Low-code KPI dashboard component." } ]; +const packOrder = [ + "system-core", + "data-table", + "file-folder", + "interaction", + "io", + "orchestrator", + "office", + "communications", + "documents", + "integration-service", + "document-intelligence", + "ai-center", + "computer-vision", + "database", + "ftp", + "mobile", + "terminal-mainframe", + "cryptography", + "discovery", + "apps" +] as const; + +const packLabels: Record = { + "system-core": "System & Core", + "data-table": "Data Table", + "file-folder": "File & Folder", + interaction: "Interaction", + io: "Input/Output", + orchestrator: "Orchestrator", + office: "Excel", + communications: "Mail", + documents: "PDF/Word", + "integration-service": "Integration Service", + "document-intelligence": "Document Understanding", + "ai-center": "AI Center / GenAI", + "computer-vision": "Computer Vision", + database: "Database", + ftp: "FTP", + mobile: "Mobile", + "terminal-mainframe": "Terminal/Mainframe", + cryptography: "Cryptography", + discovery: "Discovery", + apps: "Apps" +}; + export function listActivities() { const available = activities.filter((item) => item.status === "available"); const planned = activities.filter((item) => item.status === "planned"); @@ -60,6 +209,42 @@ export function listActivities() { acc[item.category] = (acc[item.category] || 0) + 1; return acc; }, {}); + const byPhase = activities.reduce>( + (acc, item) => { + acc[item.phase] += 1; + return acc; + }, + { "phase-1": 0, "phase-2": 0, "phase-3": 0 } + ); + const byPack = activities.reduce>((acc, item) => { + acc[item.pack] = (acc[item.pack] || 0) + 1; + return acc; + }, {}); + + const roadmap = packOrder + .filter((packId) => byPack[packId] > 0) + .map((packId) => { + const packItems = activities.filter((item) => item.pack === packId); + const availableCount = packItems.filter((item) => item.status === "available").length; + const plannedCount = packItems.length - availableCount; + const phaseRank = Math.min( + ...packItems.map((item) => { + if (item.phase === "phase-1") return 1; + if (item.phase === "phase-2") return 2; + return 3; + }) + ); + const phase: ActivityPhase = phaseRank === 1 ? "phase-1" : phaseRank === 2 ? "phase-2" : "phase-3"; + return { + id: packId, + label: packLabels[packId] || packId, + phase, + total: packItems.length, + available: availableCount, + planned: plannedCount, + activityIds: packItems.map((item) => item.id) + }; + }); return { targetLibrarySize: 300, @@ -67,6 +252,14 @@ export function listActivities() { availableCount: available.length, plannedCount: planned.length, byCategory, + byPhase, + byPack, + roadmap, + phaseFocus: { + now: "phase-1" as ActivityPhase, + next: "phase-2" as ActivityPhase, + later: "phase-3" as ActivityPhase + }, items: activities }; } diff --git a/apps/server/src/lib/templateQuality.test.ts b/apps/server/src/lib/templateQuality.test.ts new file mode 100644 index 0000000..ebb3135 --- /dev/null +++ b/apps/server/src/lib/templateQuality.test.ts @@ -0,0 +1,9 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { validateAllWorkflowTemplates } from "./templateQuality.js"; + +test("all workflow templates pass quality gate checks", () => { + const report = validateAllWorkflowTemplates(); + const failing = report.filter((item) => item.errors.length > 0); + assert.equal(failing.length, 0, failing.map((item) => `${item.templateId}: ${item.errors.join(" | ")}`).join("\n")); +}); diff --git a/apps/server/src/lib/templateQuality.ts b/apps/server/src/lib/templateQuality.ts new file mode 100644 index 0000000..68fe0a9 --- /dev/null +++ b/apps/server/src/lib/templateQuality.ts @@ -0,0 +1,85 @@ +import { listWorkflowTemplateDefinitions, type WorkflowTemplate } from "./templates.js"; + +function isNonEmptyString(value: unknown) { + return typeof value === "string" && value.trim().length > 0; +} + +export function validateWorkflowTemplate(template: WorkflowTemplate & { setup?: any }) { + const errors: string[] = []; + + if (!isNonEmptyString(template.id)) errors.push("Missing template id"); + if (!isNonEmptyString(template.name)) errors.push("Missing template name"); + if (!isNonEmptyString(template.description)) errors.push("Missing template description"); + if (!isNonEmptyString(template.category)) errors.push("Missing template category"); + if (!isNonEmptyString(template.useCase)) errors.push("Missing template useCase"); + if (!Array.isArray(template.tags) || template.tags.length === 0) errors.push("Template tags must be non-empty"); + + const definition = (template.definition || {}) as Record; + const nodes = Array.isArray(definition.nodes) ? (definition.nodes as Array>) : []; + const edges = Array.isArray(definition.edges) ? (definition.edges as Array>) : []; + + if (!nodes.length) errors.push("Definition must include at least one node"); + if (!edges.length) errors.push("Definition must include at least one edge"); + + const nodeIds = new Set(); + for (const node of nodes) { + const nodeId = typeof node?.id === "string" ? node.id.trim() : ""; + if (!nodeId) { + errors.push("Node is missing id"); + continue; + } + if (nodeIds.has(nodeId)) errors.push(`Duplicate node id: ${nodeId}`); + nodeIds.add(nodeId); + } + + const hasStart = nodes.some((node) => String(node?.data?.type || "") === "start"); + if (!hasStart) errors.push("Definition must include a start node"); + + const edgeIds = new Set(); + for (const edge of edges) { + const edgeId = typeof edge?.id === "string" ? edge.id.trim() : ""; + const source = typeof edge?.source === "string" ? edge.source.trim() : ""; + const target = typeof edge?.target === "string" ? edge.target.trim() : ""; + if (!edgeId) errors.push("Edge is missing id"); + if (edgeId && edgeIds.has(edgeId)) errors.push(`Duplicate edge id: ${edgeId}`); + if (edgeId) edgeIds.add(edgeId); + if (!source || !target) { + errors.push(`Edge ${edgeId || ""} is missing source/target`); + continue; + } + if (!nodeIds.has(source)) errors.push(`Edge ${edgeId} source does not exist: ${source}`); + if (!nodeIds.has(target)) errors.push(`Edge ${edgeId} target does not exist: ${target}`); + } + + const execution = (definition.execution || {}) as Record; + const requiredExecutionNumbers = ["globalTimeoutMs", "defaultRetries", "defaultNodeTimeoutMs"] as const; + for (const key of requiredExecutionNumbers) { + const value = execution[key]; + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + errors.push(`Execution.${key} must be a non-negative number`); + } + } + + const setup = template.setup as Record | undefined; + if (!setup || typeof setup !== "object") { + errors.push("Template setup guide is required"); + } else { + const requiredInputs = Array.isArray(setup.requiredInputs) ? setup.requiredInputs : []; + const connectionChecks = Array.isArray(setup.connectionChecks) ? setup.connectionChecks : []; + const sampleInput = setup.sampleInput; + if (requiredInputs.length === 0) errors.push("Template setup requires at least one required/checklist input"); + if (connectionChecks.length === 0) errors.push("Template setup requires at least one connection check"); + if (!sampleInput || typeof sampleInput !== "object" || Array.isArray(sampleInput)) { + errors.push("Template setup sampleInput must be an object"); + } + } + + return errors; +} + +export function validateAllWorkflowTemplates() { + return listWorkflowTemplateDefinitions().map((template) => ({ + templateId: template.id, + errors: validateWorkflowTemplate(template) + })); +} diff --git a/apps/server/src/lib/templates.test.ts b/apps/server/src/lib/templates.test.ts index 2a09e69..cfed338 100644 --- a/apps/server/src/lib/templates.test.ts +++ b/apps/server/src/lib/templates.test.ts @@ -18,6 +18,8 @@ test("listWorkflowTemplates exposes curated templates", () => { const sample = templates.find((template) => template.id === "invoice-intake-approval"); assert.equal(typeof sample?.difficulty, "string"); assert.equal(Array.isArray(sample?.tags), true); + assert.equal(Array.isArray((sample as any)?.setup?.requiredInputs), true); + assert.equal(Array.isArray((sample as any)?.setup?.connectionChecks), true); }); test("getWorkflowTemplate returns full definition for known template", () => { @@ -25,4 +27,5 @@ test("getWorkflowTemplate returns full definition for known template", () => { assert.ok(template); assert.equal(template?.name, "Web Scrape -> API Sync"); assert.equal(Array.isArray((template?.definition as any).nodes), true); + assert.equal(typeof (template as any)?.setup?.sampleInput, "object"); }); diff --git a/apps/server/src/lib/templates.ts b/apps/server/src/lib/templates.ts index b14dfb3..72220d7 100644 --- a/apps/server/src/lib/templates.ts +++ b/apps/server/src/lib/templates.ts @@ -6,9 +6,34 @@ export type WorkflowTemplate = { difficulty: "starter" | "intermediate" | "advanced"; useCase: string; tags: string[]; + setup?: TemplateSetupGuide; definition: Record; }; +export type TemplateSetupField = { + id: string; + label: string; + kind: "text" | "url" | "integration" | "selector" | "json"; + required: boolean; + placeholder?: string; + defaultValue?: string; + help?: string; +}; + +export type TemplateSetupCheck = { + id: string; + label: string; + type: "preflight" | "integration"; + integrationId?: string; +}; + +export type TemplateSetupGuide = { + requiredInputs: TemplateSetupField[]; + connectionChecks: TemplateSetupCheck[]; + sampleInput: Record; + runbook: string[]; +}; + const executionDefaults = { globalTimeoutMs: 1800000, defaultRetries: 2, @@ -24,6 +49,45 @@ const templates: WorkflowTemplate[] = [ difficulty: "intermediate", useCase: "Invoice capture and governance workflow with human oversight.", tags: ["invoice", "document", "approval", "integration"], + setup: { + requiredInputs: [ + { + id: "finance_api", + label: "Finance integration profile", + kind: "integration", + required: true, + defaultValue: "finance_api", + help: "Create an integration profile used by the sync node." + }, + { + id: "approval_threshold", + label: "Approval threshold amount", + kind: "text", + required: true, + defaultValue: "10000", + placeholder: "10000" + }, + { + id: "invoice_fields", + label: "Required invoice fields", + kind: "text", + required: true, + defaultValue: "invoice_number,vendor,total,due_date,currency" + } + ], + connectionChecks: [ + { id: "invoice-preflight", label: "Template preflight readiness", type: "preflight" }, + { id: "invoice-finance-integration", label: "Finance integration exists", type: "integration", integrationId: "finance_api" } + ], + sampleInput: { + invoiceRawText: "Invoice Number: INV-2026-0042\nVendor: Example AB\nTotal: 12850.00\nCurrency: USD\nDue Date: 2026-03-01" + }, + runbook: [ + "Replace sample invoice text with source data from your intake channel.", + "Confirm approval policy/threshold before publishing.", + "Run in test mode once and verify sync payload." + ] + }, definition: { nodes: [ { id: "start", type: "action", position: { x: 80, y: 80 }, data: { type: "start", label: "Start" } }, @@ -157,6 +221,41 @@ const templates: WorkflowTemplate[] = [ difficulty: "intermediate", useCase: "Move structured data from browser UI into backend systems.", tags: ["scrape", "playwright", "api", "sync"], + setup: { + requiredInputs: [ + { + id: "source_url", + label: "Source portal URL", + kind: "url", + required: true, + defaultValue: "https://example.com/reports/orders" + }, + { + id: "table_selector", + label: "Table selector", + kind: "selector", + required: true, + defaultValue: "table tbody" + }, + { + id: "target_api_url", + label: "Target API URL", + kind: "url", + required: true, + defaultValue: "https://example.com/api/order-sync" + } + ], + connectionChecks: [{ id: "web-sync-preflight", label: "Template preflight readiness", type: "preflight" }], + sampleInput: { + source: "orders-report", + trigger: "manual" + }, + runbook: [ + "Update selectors against the live page before running unattended.", + "Confirm API auth and request body schema.", + "Run once in test mode and inspect extracted rows." + ] + }, definition: { nodes: [ { id: "start", type: "action", position: { x: 80, y: 80 }, data: { type: "start", label: "Start" } }, @@ -235,6 +334,33 @@ const templates: WorkflowTemplate[] = [ difficulty: "starter", useCase: "Prepare spreadsheet exports for reliable downstream ingestion.", tags: ["csv", "cleanup", "validation", "etl"], + setup: { + requiredInputs: [ + { + id: "csv_schema", + label: "Expected CSV columns", + kind: "text", + required: true, + defaultValue: "id,email,total" + }, + { + id: "import_api_url", + label: "Import API URL", + kind: "url", + required: true, + defaultValue: "https://example.com/api/csv-import" + } + ], + connectionChecks: [{ id: "csv-preflight", label: "Template preflight readiness", type: "preflight" }], + sampleInput: { + csvText: "id,email,total\n1,alice@example.com,120.50\n2,bob@example.com,94.00" + }, + runbook: [ + "Replace starter CSV rows with representative production samples.", + "Extend validation schema to include required business fields.", + "Run in test mode and confirm rejected-row handling." + ] + }, definition: { nodes: [ { id: "start", type: "action", position: { x: 80, y: 80 }, data: { type: "start", label: "Start" } }, @@ -306,6 +432,36 @@ const templates: WorkflowTemplate[] = [ difficulty: "intermediate", useCase: "Convert inbox workload into structured ticket operations.", tags: ["email", "triage", "ticketing", "approval"], + setup: { + requiredInputs: [ + { + id: "helpdesk_api", + label: "Helpdesk integration profile", + kind: "integration", + required: true, + defaultValue: "helpdesk_api" + }, + { + id: "priority_labels", + label: "Priority labels", + kind: "text", + required: true, + defaultValue: "high,medium,low" + } + ], + connectionChecks: [ + { id: "email-preflight", label: "Template preflight readiness", type: "preflight" }, + { id: "email-helpdesk-integration", label: "Helpdesk integration exists", type: "integration", integrationId: "helpdesk_api" } + ], + sampleInput: { + emailBody: "Subject: Unable to process payroll export\nPriority: high\nBody: payroll API returns 500 for 3 hours." + }, + runbook: [ + "Tune triage prompt for your support taxonomy.", + "Map ticket payload fields to your destination system.", + "Run test mode and verify approval path for high priority." + ] + }, definition: { nodes: [ { id: "start", type: "action", position: { x: 80, y: 80 }, data: { type: "start", label: "Start" } }, @@ -410,6 +566,33 @@ const templates: WorkflowTemplate[] = [ difficulty: "starter", useCase: "Operational monitoring workflow for scheduled execution.", tags: ["health-check", "alerting", "ops", "schedule"], + setup: { + requiredInputs: [ + { + id: "health_url", + label: "Health endpoint URL", + kind: "url", + required: true, + defaultValue: "https://example.com/health" + }, + { + id: "alert_webhook", + label: "Alert webhook URL", + kind: "url", + required: true, + defaultValue: "https://example.com/webhooks/alerts" + } + ], + connectionChecks: [{ id: "health-preflight", label: "Template preflight readiness", type: "preflight" }], + sampleInput: { + source: "scheduled-health-check" + }, + runbook: [ + "Attach this workflow to a schedule preset before enabling unattended mode.", + "Verify alert endpoint ownership and retry policy.", + "Run a simulated degraded response to validate routing." + ] + }, definition: { nodes: [ { id: "start", type: "action", position: { x: 80, y: 80 }, data: { type: "start", label: "Start" } }, @@ -972,8 +1155,107 @@ const templates: WorkflowTemplate[] = [ } ]; +function sanitizeId(input: string) { + return input.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, ""); +} + +function inferTemplateSetup(template: WorkflowTemplate): TemplateSetupGuide { + const nodes = Array.isArray((template.definition as any)?.nodes) ? ((template.definition as any).nodes as Array>) : []; + const integrationIds = new Set(); + const placeholders = new Set(); + + const collect = (value: unknown) => { + if (typeof value === "string") { + for (const match of value.matchAll(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g)) { + if (match[1]) placeholders.add(match[1]); + } + return; + } + if (Array.isArray(value)) { + for (const item of value) collect(item); + return; + } + if (value && typeof value === "object") { + for (const nested of Object.values(value as Record)) collect(nested); + } + }; + + for (const node of nodes) { + const data = node?.data as Record | undefined; + if (!data) continue; + const integrationId = typeof data.integrationId === "string" ? data.integrationId.trim() : ""; + if (integrationId) integrationIds.add(integrationId); + collect(data); + } + + const requiredInputs: TemplateSetupField[] = [ + ...Array.from(integrationIds).map((integrationId) => ({ + id: `integration-${sanitizeId(integrationId)}`, + label: `Integration profile: ${integrationId}`, + kind: "integration" as const, + required: true, + defaultValue: integrationId + })), + ...Array.from(placeholders) + .slice(0, 3) + .map((key) => ({ + id: `input-${sanitizeId(key)}`, + label: `Input value: ${key}`, + kind: "text" as const, + required: false, + placeholder: `sample-${key}` + })) + ]; + if (!requiredInputs.length) { + requiredInputs.push({ + id: "review-notes", + label: "Template review notes", + kind: "text", + required: false, + placeholder: "record any selector/endpoint adjustments before first run" + }); + } + + const sampleInput = Array.from(placeholders) + .slice(0, 5) + .reduce>((acc, key) => { + acc[key] = `sample-${key}`; + return acc; + }, {}); + + return { + requiredInputs, + connectionChecks: [ + { id: `${template.id}-preflight`, label: "Template preflight readiness", type: "preflight" }, + ...Array.from(integrationIds).map((integrationId) => ({ + id: `${template.id}-integration-${sanitizeId(integrationId)}`, + label: `Integration exists: ${integrationId}`, + type: "integration" as const, + integrationId + })) + ], + sampleInput: Object.keys(sampleInput).length ? sampleInput : { source: "demo" }, + runbook: [ + "Fill required setup values before first run.", + "Run in test mode and inspect logs/context outputs.", + "Publish only after successful test validation." + ] + }; +} + +function withResolvedSetup(template: WorkflowTemplate): WorkflowTemplate & { setup: TemplateSetupGuide } { + return { + ...template, + setup: template.setup || inferTemplateSetup(template) + }; +} + +export function listWorkflowTemplateDefinitions() { + return templates.map((template) => withResolvedSetup(template)); +} + export function listWorkflowTemplates() { - return templates.map((template) => ({ + return listWorkflowTemplateDefinitions().map((template) => ({ id: template.id, name: template.name, description: template.description, @@ -981,10 +1263,12 @@ export function listWorkflowTemplates() { difficulty: template.difficulty, useCase: template.useCase, tags: template.tags, + setup: template.setup, nodes: Array.isArray((template.definition as any)?.nodes) ? (template.definition as any).nodes.length : 0 })); } export function getWorkflowTemplate(templateId: string) { - return templates.find((template) => template.id === templateId) || null; + const template = templates.find((item) => item.id === templateId); + return template ? withResolvedSetup(template) : null; } diff --git a/apps/web/e2e/mockApi.ts b/apps/web/e2e/mockApi.ts index ae7337e..ed01f7a 100644 --- a/apps/web/e2e/mockApi.ts +++ b/apps/web/e2e/mockApi.ts @@ -217,15 +217,39 @@ export async function installMockApi(page: Page) { id: "template-demo", name: "Demo Template", category: "General", - difficulty: "easy", + difficulty: "starter", nodes: 3, description: "Demo template", useCase: "Smoke", tags: ["demo"], - definition: defaultDefinition() + setup: { + requiredInputs: [{ id: "api_url", label: "API URL", kind: "url", required: true, defaultValue: "https://example.com" }], + connectionChecks: [{ id: "preflight", label: "Template preflight readiness", type: "preflight" }], + sampleInput: { source: "smoke" }, + runbook: ["Run in test mode first"] + } } ]); } + if (path === "/api/templates/template-demo" && method === "GET") { + return json(route, { + id: "template-demo", + name: "Demo Template", + category: "General", + difficulty: "starter", + nodes: 3, + description: "Demo template", + useCase: "Smoke", + tags: ["demo"], + setup: { + requiredInputs: [{ id: "api_url", label: "API URL", kind: "url", required: true, defaultValue: "https://example.com" }], + connectionChecks: [{ id: "preflight", label: "Template preflight readiness", type: "preflight" }], + sampleInput: { source: "smoke" }, + runbook: ["Run in test mode first"] + }, + definition: defaultDefinition() + }); + } if (path === "/api/activities" && method === "GET") { return json(route, { targetLibrarySize: 300, @@ -233,6 +257,11 @@ export async function installMockApi(page: Page) { availableCount: 20, plannedCount: 10, byCategory: { Core: 8, Web: 6, AI: 4, Data: 6, Control: 6 }, + byPhase: { "phase-1": 20, "phase-2": 10, "phase-3": 0 }, + roadmap: [ + { id: "system-core", label: "System & Core", phase: "phase-1", total: 10, available: 6, planned: 4, activityIds: [] } + ], + phaseFocus: { now: "phase-1", next: "phase-2", later: "phase-3" }, items: [] }); } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 551854b..81b388f 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -55,6 +55,7 @@ import { getUpcomingSchedules, getSecrets, getSystemTime, + getTemplate, getTemplates, getWebhookEvents, getWebhooks, @@ -94,14 +95,23 @@ import { Sidebar } from "./components/Sidebar"; import { Inspector } from "./components/Inspector"; import { useGlobalHotkeys } from "./hooks/useGlobalHotkeys"; import { filterNodeOptions, NODE_OPTIONS } from "./lib/nodeCatalog"; +import { STARTER_WALKTHROUGH_STEPS, clampWalkthroughIndex } from "./lib/starterWalkthrough"; +import { + buildTemplateReadiness, + buildTemplateSetupInitialValues, + integrationExists +} from "./lib/templateSetup"; import { buildPersistedDefinition, hashDefinition } from "./lib/workflowDraft"; import type { ActivityCatalog, + ActivityRoadmapPack, AutopilotPlan, MiningSummary, OrchestratorJob, OrchestratorOverview, OrchestratorRobot, + WorkflowTemplateDetail, + WorkflowTemplateSummary, WorkflowDefinition, WorkflowRecord, WorkflowRunDetail, @@ -111,6 +121,7 @@ import type { } from "./types"; const nodeTypes = { action: ActionNode }; +const WALKTHROUGH_DISMISSED_KEY = "forgeflow.walkthrough.dismissed.v1"; type ToastLevel = "info" | "success" | "error"; type ToastAction = { label: string; @@ -177,6 +188,11 @@ type RecorderDraftEvent = { text: string; }; +type TemplatePreflightState = { + status: "idle" | "running" | "pass" | "fail"; + messages: string[]; +}; + type WorkflowPresence = { clientId: string; workflowId: string; @@ -370,10 +386,14 @@ export default function App() { const [secretKey, setSecretKey] = useState(""); const [secretValue, setSecretValue] = useState(""); const [workflowName, setWorkflowName] = useState(""); - const [templates, setTemplates] = useState([]); + const [templates, setTemplates] = useState([]); + const [templateDetailsById, setTemplateDetailsById] = useState>({}); const [selectedTemplateId, setSelectedTemplateId] = useState(""); const [templateWorkflowName, setTemplateWorkflowName] = useState(""); const [templateSearch, setTemplateSearch] = useState(""); + const [templateSetupValues, setTemplateSetupValues] = useState>({}); + const [templatePreflight, setTemplatePreflight] = useState({ status: "idle", messages: [] }); + const [templateSampleCopied, setTemplateSampleCopied] = useState(false); const [autopilotPrompt, setAutopilotPrompt] = useState(""); const [autopilotWorkflowName, setAutopilotWorkflowName] = useState(""); const [autopilotPlanDraft, setAutopilotPlanDraft] = useState(null); @@ -441,6 +461,9 @@ export default function App() { const [twoFactorStatus, setTwoFactorStatus] = useState(null); const [twoFactorSetup, setTwoFactorSetup] = useState(null); const [twoFactorToken, setTwoFactorToken] = useState(""); + const [walkthroughActive, setWalkthroughActive] = useState(false); + const [walkthroughStepIndex, setWalkthroughStepIndex] = useState(0); + const [walkthroughDismissed, setWalkthroughDismissed] = useState(false); const nodesRef = useRef(nodes); const edgesRef = useRef(edges); @@ -610,6 +633,57 @@ export default function App() { () => (templates || []).find((template) => template.id === selectedTemplateId) || null, [templates, selectedTemplateId] ); + const selectedTemplateDetail = useMemo( + () => (selectedTemplateId ? templateDetailsById[selectedTemplateId] || null : null), + [templateDetailsById, selectedTemplateId] + ); + const selectedTemplateSetup = useMemo( + () => selectedTemplateDetail?.setup || selectedTemplate?.setup || null, + [selectedTemplateDetail, selectedTemplate] + ); + const templateSetupReadiness = useMemo( + () => + buildTemplateReadiness({ + setup: selectedTemplateSetup, + values: templateSetupValues, + integrations, + preflightReady: templatePreflight.status === "pass" + }), + [selectedTemplateSetup, templateSetupValues, integrations, templatePreflight.status] + ); + const activityRoadmapTop = useMemo( + () => ((activityCatalog?.roadmap || []) as ActivityRoadmapPack[]).slice(0, 6), + [activityCatalog?.roadmap] + ); + const walkthroughStep = useMemo( + () => STARTER_WALKTHROUGH_STEPS[clampWalkthroughIndex(walkthroughStepIndex)] || STARTER_WALKTHROUGH_STEPS[0], + [walkthroughStepIndex] + ); + + useEffect(() => { + if (!token) return; + const dismissed = localStorage.getItem(WALKTHROUGH_DISMISSED_KEY) === "1"; + setWalkthroughDismissed(dismissed); + if (!dismissed) { + setWalkthroughActive(true); + setWalkthroughStepIndex(0); + } + }, [token]); + + useEffect(() => { + if (!selectedTemplateId || templateDetailsById[selectedTemplateId]) return; + getTemplate(selectedTemplateId) + .then((detail) => { + setTemplateDetailsById((current) => ({ ...current, [selectedTemplateId]: detail as WorkflowTemplateDetail })); + }) + .catch(showError); + }, [selectedTemplateId, templateDetailsById]); + + useEffect(() => { + setTemplateSampleCopied(false); + setTemplatePreflight({ status: "idle", messages: [] }); + setTemplateSetupValues(buildTemplateSetupInitialValues(selectedTemplateSetup)); + }, [selectedTemplateId, selectedTemplateSetup]); const activeSchedulePreset = useMemo( () => schedulePresets.find((preset) => preset.id === selectedSchedulePreset) || null, @@ -773,9 +847,10 @@ export default function App() { .catch(showError); getTemplates() .then((list) => { - setTemplates(list || []); - if (Array.isArray(list) && list.length && !selectedTemplateId) { - setSelectedTemplateId(list[0].id); + const typed = Array.isArray(list) ? (list as WorkflowTemplateSummary[]) : []; + setTemplates(typed); + if (typed.length && !selectedTemplateId) { + setSelectedTemplateId(typed[0].id); } }) .catch(showError); @@ -1236,11 +1311,86 @@ export default function App() { await refreshDashboard(); }; + const handleTemplateSetupFieldChange = (fieldId: string, value: string) => { + setTemplateSetupValues((current) => ({ ...current, [fieldId]: value })); + }; + + const handleValidateTemplateSetup = async () => { + if (!selectedTemplateDetail?.definition) { + setFeedback("Template details are still loading", "info"); + return; + } + setTemplatePreflight({ status: "running", messages: [] }); + const result = await runPreflight({ definition: selectedTemplateDetail.definition }); + setTemplatePreflight({ + status: result.ready ? "pass" : "fail", + messages: Array.isArray(result.messages) ? result.messages.map((item) => String(item)) : [] + }); + if (result.ready) { + setFeedback("Template setup preflight passed", "success"); + } else { + const msg = Array.isArray(result.messages) && result.messages.length ? result.messages.join(" | ") : "Preflight failed"; + setFeedback(`Template setup preflight blocked: ${msg}`, "error"); + } + }; + + const handleCopyTemplateSampleInput = async () => { + const sampleInput = selectedTemplateSetup?.sampleInput; + if (!sampleInput) { + setFeedback("No sample input available for selected template", "info"); + return; + } + try { + if (!navigator.clipboard?.writeText) { + throw new Error("Clipboard API unavailable"); + } + await navigator.clipboard.writeText(JSON.stringify(sampleInput, null, 2)); + setTemplateSampleCopied(true); + setFeedback("Template sample input copied", "success"); + } catch { + setFeedback("Unable to copy sample input", "error"); + } + }; + + const handleStartWalkthrough = () => { + setWalkthroughDismissed(false); + localStorage.removeItem(WALKTHROUGH_DISMISSED_KEY); + setWalkthroughActive(true); + setWalkthroughStepIndex(0); + scrollSidebarSection(STARTER_WALKTHROUGH_STEPS[0].sectionId); + }; + + const handleWalkthroughDismiss = () => { + setWalkthroughDismissed(true); + setWalkthroughActive(false); + localStorage.setItem(WALKTHROUGH_DISMISSED_KEY, "1"); + }; + + const handleWalkthroughNext = () => { + const next = clampWalkthroughIndex(walkthroughStepIndex + 1); + setWalkthroughStepIndex(next); + scrollSidebarSection(STARTER_WALKTHROUGH_STEPS[next].sectionId); + }; + + const handleWalkthroughPrev = () => { + const next = clampWalkthroughIndex(walkthroughStepIndex - 1); + setWalkthroughStepIndex(next); + scrollSidebarSection(STARTER_WALKTHROUGH_STEPS[next].sectionId); + }; + const handleCreateFromTemplate = async () => { if (!selectedTemplateId) { setFeedback("Select a template first", "error"); return; } + if (!templateSetupReadiness.requiredComplete) { + setFeedback("Complete required template setup fields before creating workflow", "error"); + return; + } + if (selectedTemplateSetup?.connectionChecks?.length && !templateSetupReadiness.checksReady) { + setFeedback("Template setup checks are not ready yet (run preflight/check integrations)", "error"); + return; + } const created = await createWorkflowFromTemplate({ templateId: selectedTemplateId, name: templateWorkflowName.trim() || undefined @@ -1249,6 +1399,9 @@ export default function App() { setWorkflowList((list) => [created, ...list]); await selectWorkflow(created); setFeedback("Workflow created from template", "success"); + if (walkthroughActive) { + setWalkthroughStepIndex((current) => clampWalkthroughIndex(current + 1)); + } await refreshDashboard(); }; @@ -1320,6 +1473,10 @@ export default function App() { setVersions([]); setSecrets([]); setTemplates([]); + setTemplateDetailsById({}); + setTemplateSetupValues({}); + setTemplatePreflight({ status: "idle", messages: [] }); + setTemplateSampleCopied(false); setActivityCatalog(null); setAutopilotPrompt(""); setAutopilotWorkflowName(""); @@ -1349,6 +1506,9 @@ export default function App() { setTwoFactorStatus(null); setTwoFactorSetup(null); setTwoFactorToken(""); + setWalkthroughActive(false); + setWalkthroughStepIndex(0); + setWalkthroughDismissed(false); setLoginTotpCode(""); setSelectedNode(null); setSelectedEdgeId(null); @@ -2684,6 +2844,47 @@ export default function App() { User: {currentUser?.username || "-"} ({currentUser?.role || "unknown"}) +
+ Starter Walkthrough + {!walkthroughActive ? ( + <> + {walkthroughDismissed ? "Walkthrough hidden for this browser." : "Run the first-time guided setup flow."} +
+ + {!walkthroughDismissed ? ( + + ) : null} +
+ + ) : ( + <> + + Step {clampWalkthroughIndex(walkthroughStepIndex) + 1} / {STARTER_WALKTHROUGH_STEPS.length} + + {walkthroughStep.title} + {walkthroughStep.description} +
+ + + + +
+ + )} +
{sidebarShortcuts.map((item) => (
+ ) : null} + {selectedTemplateSetup ? ( +
+ Template Setup Wizard + {(selectedTemplateSetup.requiredInputs || []).map((field) => ( +
+ + {field.label} + {field.required ? " *" : ""} + + handleTemplateSetupFieldChange(field.id, e.target.value)} + placeholder={field.placeholder || field.defaultValue || ""} + /> + {field.help ? {field.help} : null} +
+ ))} + {(selectedTemplateSetup.connectionChecks || []).length ? Connection checks : null} + {(selectedTemplateSetup.connectionChecks || []).map((check) => { + const ok = check.type === "preflight" ? templatePreflight.status === "pass" : integrationExists(check.integrationId, integrations); + return ( + + {ok ? "PASS" : "PENDING"} - {check.label} + + ); + })} + {templatePreflight.messages.length ? ( +
+ {templatePreflight.messages.slice(0, 3).map((message, index) => ( + {message} + ))} +
+ ) : null} +
+ + +
+ Sample input is copied as JSON for test-mode runs.
) : null} >( - "/api/templates" - ); + return request("/api/templates"); +} + +export function getTemplate(templateId: string) { + return request(`/api/templates/${encodeURIComponent(templateId)}`); } export function getActivities() { diff --git a/apps/web/src/lib/starterWalkthrough.test.ts b/apps/web/src/lib/starterWalkthrough.test.ts new file mode 100644 index 0000000..543fc95 --- /dev/null +++ b/apps/web/src/lib/starterWalkthrough.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import { STARTER_WALKTHROUGH_STEPS, clampWalkthroughIndex } from "./starterWalkthrough"; + +describe("starter walkthrough", () => { + test("has stable step sequence", () => { + expect(STARTER_WALKTHROUGH_STEPS.length).toBeGreaterThanOrEqual(5); + expect(STARTER_WALKTHROUGH_STEPS[0].sectionId).toBe("templates"); + }); + + test("clampWalkthroughIndex keeps values in range", () => { + expect(clampWalkthroughIndex(-10)).toBe(0); + expect(clampWalkthroughIndex(100)).toBe(STARTER_WALKTHROUGH_STEPS.length - 1); + }); +}); diff --git a/apps/web/src/lib/starterWalkthrough.ts b/apps/web/src/lib/starterWalkthrough.ts new file mode 100644 index 0000000..9e2b724 --- /dev/null +++ b/apps/web/src/lib/starterWalkthrough.ts @@ -0,0 +1,44 @@ +export type StarterWalkthroughStep = { + id: string; + sectionId: string; + title: string; + description: string; +}; + +export const STARTER_WALKTHROUGH_STEPS: StarterWalkthroughStep[] = [ + { + id: "template", + sectionId: "templates", + title: "Pick a Starter Template", + description: "Choose a production starter, review setup requirements, and name your workflow." + }, + { + id: "setup", + sectionId: "templates", + title: "Complete Template Setup", + description: "Fill required setup fields, confirm integrations, and run template preflight validation." + }, + { + id: "integrations", + sectionId: "integrations", + title: "Verify Integrations", + description: "Create or test required integration profiles before your first run." + }, + { + id: "run", + sectionId: "workflow", + title: "Run in Test Mode", + description: "Start a test run and inspect diagnostics and context outputs." + }, + { + id: "publish", + sectionId: "versions", + title: "Publish and Schedule", + description: "Publish after a green test run, then attach schedule/orchestrator settings." + } +]; + +export function clampWalkthroughIndex(index: number) { + if (!Number.isFinite(index)) return 0; + return Math.max(0, Math.min(STARTER_WALKTHROUGH_STEPS.length - 1, Math.floor(index))); +} diff --git a/apps/web/src/lib/templateSetup.test.ts b/apps/web/src/lib/templateSetup.test.ts new file mode 100644 index 0000000..a9a4f79 --- /dev/null +++ b/apps/web/src/lib/templateSetup.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "vitest"; +import { + buildTemplateReadiness, + buildTemplateSetupInitialValues, + buildTemplateSetupProgress, + integrationExists +} from "./templateSetup"; + +describe("template setup helpers", () => { + const setup = { + requiredInputs: [ + { id: "api_url", label: "API URL", kind: "url", required: true, defaultValue: "https://example.com" }, + { id: "notes", label: "Notes", kind: "text", required: false } + ], + connectionChecks: [ + { id: "preflight", label: "Preflight", type: "preflight" as const }, + { id: "integration", label: "Integration", type: "integration" as const, integrationId: "finance_api" } + ], + sampleInput: { source: "demo" }, + runbook: ["Review setup"] + }; + + test("buildTemplateSetupInitialValues maps defaults", () => { + const values = buildTemplateSetupInitialValues(setup); + expect(values.api_url).toBe("https://example.com"); + expect(values.notes).toBe(""); + }); + + test("buildTemplateSetupProgress counts required fields", () => { + const progress = buildTemplateSetupProgress(setup, { api_url: " ", notes: "ok" }); + expect(progress.requiredTotal).toBe(1); + expect(progress.requiredDone).toBe(0); + expect(progress.requiredComplete).toBe(false); + }); + + test("integrationExists checks id and name", () => { + expect(integrationExists("finance_api", [{ id: "finance_api" }])).toBe(true); + expect(integrationExists("finance_api", [{ name: "finance_api" }])).toBe(true); + expect(integrationExists("finance_api", [{ id: "other" }])).toBe(false); + }); + + test("buildTemplateReadiness requires required fields and checks", () => { + const readiness = buildTemplateReadiness({ + setup, + values: { api_url: "https://example.com" }, + integrations: [{ id: "finance_api" }], + preflightReady: true + }); + expect(readiness.ready).toBe(true); + expect(readiness.checksReady).toBe(true); + }); +}); diff --git a/apps/web/src/lib/templateSetup.ts b/apps/web/src/lib/templateSetup.ts new file mode 100644 index 0000000..3d55105 --- /dev/null +++ b/apps/web/src/lib/templateSetup.ts @@ -0,0 +1,66 @@ +import type { TemplateSetupGuide } from "../types"; + +type IntegrationLike = { id?: string; name?: string; type?: string }; + +export function buildTemplateSetupInitialValues(setup?: TemplateSetupGuide | null) { + const next: Record = {}; + for (const field of setup?.requiredInputs || []) { + next[field.id] = field.defaultValue || ""; + } + return next; +} + +export function buildTemplateSetupProgress(setup: TemplateSetupGuide | null | undefined, values: Record) { + const requiredFields = (setup?.requiredInputs || []).filter((field) => field.required); + const requiredTotal = requiredFields.length; + const requiredDone = requiredFields.filter((field) => String(values[field.id] || "").trim().length > 0).length; + return { + requiredTotal, + requiredDone, + requiredComplete: requiredTotal === 0 || requiredDone >= requiredTotal + }; +} + +export function integrationExists( + integrationId: string | undefined, + integrations: IntegrationLike[] | null | undefined +): boolean { + if (!integrationId) return true; + const target = integrationId.trim().toLowerCase(); + if (!target) return true; + return (integrations || []).some((item) => { + const byId = String(item?.id || "").trim().toLowerCase(); + const byName = String(item?.name || "").trim().toLowerCase(); + return byId === target || byName === target; + }); +} + +export function buildTemplateReadiness(args: { + setup?: TemplateSetupGuide | null; + values?: Record; + integrations?: IntegrationLike[] | null; + preflightReady?: boolean; +}) { + const setup = args.setup || null; + const values = args.values || {}; + const preflightReady = Boolean(args.preflightReady); + const progress = buildTemplateSetupProgress(setup, values); + + const checkResults = (setup?.connectionChecks || []).map((check) => { + if (check.type === "preflight") { + return { id: check.id, ok: preflightReady }; + } + return { + id: check.id, + ok: integrationExists(check.integrationId, args.integrations) + }; + }); + + const checksReady = checkResults.every((item) => item.ok); + return { + ...progress, + checksReady, + ready: progress.requiredComplete && checksReady, + checkResults + }; +} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 440b7e6..984327e 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -290,6 +290,23 @@ button.is-loading { color: var(--muted); } +.template-setup-field { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 4px; +} + +.walkthrough-panel { + border: 1px solid rgba(44, 185, 176, 0.35); + background: rgba(44, 185, 176, 0.08); + border-radius: 12px; + padding: 10px; + display: flex; + flex-direction: column; + gap: 6px; +} + .autopilot-plan { border: 1px solid rgba(44, 185, 176, 0.35); background: rgba(44, 185, 176, 0.08); diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index 3f91ac5..a186835 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -73,20 +73,80 @@ export type ActivityCatalogItem = { id: string; label: string; category: string; + pack?: string; + phase?: "phase-1" | "phase-2" | "phase-3"; status: "available" | "planned"; description: string; aliases?: string[]; }; +export type ActivityRoadmapPack = { + id: string; + label: string; + phase: "phase-1" | "phase-2" | "phase-3"; + total: number; + available: number; + planned: number; + activityIds: string[]; +}; + export type ActivityCatalog = { targetLibrarySize: number; currentTotal: number; availableCount: number; plannedCount: number; byCategory: Record; + byPhase?: Record; + byPack?: Record; + roadmap?: ActivityRoadmapPack[]; + phaseFocus?: { + now: "phase-1" | "phase-2" | "phase-3"; + next: "phase-1" | "phase-2" | "phase-3"; + later: "phase-1" | "phase-2" | "phase-3"; + }; items: ActivityCatalogItem[]; }; +export type TemplateSetupField = { + id: string; + label: string; + kind: "text" | "url" | "integration" | "selector" | "json"; + required: boolean; + placeholder?: string; + defaultValue?: string; + help?: string; +}; + +export type TemplateSetupCheck = { + id: string; + label: string; + type: "preflight" | "integration"; + integrationId?: string; +}; + +export type TemplateSetupGuide = { + requiredInputs: TemplateSetupField[]; + connectionChecks: TemplateSetupCheck[]; + sampleInput: Record; + runbook: string[]; +}; + +export type WorkflowTemplateSummary = { + id: string; + name: string; + category: string; + description?: string; + difficulty?: string; + useCase?: string; + tags?: string[]; + nodes?: number; + setup?: TemplateSetupGuide; +}; + +export type WorkflowTemplateDetail = WorkflowTemplateSummary & { + definition: WorkflowDefinition; +}; + export type AutopilotPlan = { name: string; description: string; diff --git a/docs/ACTIVITY_PACK_ROADMAP.md b/docs/ACTIVITY_PACK_ROADMAP.md new file mode 100644 index 0000000..b3762c0 --- /dev/null +++ b/docs/ACTIVITY_PACK_ROADMAP.md @@ -0,0 +1,41 @@ +# Activity Pack Roadmap + +This roadmap prioritizes activity expansion to keep ForgeFlow useful without adding low-value duplicate nodes. + +## Phase Focus + +- `phase-1` (Now): System/Core, Data Table, File & Folder, Interaction, Input/Output, Orchestrator basics. +- `phase-2` (Next): Excel, Mail, PDF/Word, Integration Service connectors, Document Understanding scopes, Database, FTP, Mobile, AI Center helpers. +- `phase-3` (Later): Terminal/Mainframe, Cryptography hardening, Apps builder expansion, advanced image analysis. + +## Pack Coverage + +- System/Core: `Assign`, `Delay`, `Do While`, `If`, `Switch`, `Parallel`, `For Each`, `Retry Scope`, `Trigger Scope`. +- Data Table: build/filter/sort/lookup/add-row/add-column/output. +- File & Folder: copy/create/delete/move/rename/append/path-exists. +- Interaction + I/O: application/browser scope, click/type/hover/state checks, clipboard, shortcuts, screenshots. +- Orchestrator: assets, queue transactions, attended/unattended triggers. +- Office/Docs: Excel, Mail, PDF, Word families. +- Integration Service: ServiceNow, Jira, Salesforce, Slack, Microsoft 365, Google Workspace. +- AI/DU/CV: document taxonomy + extraction scopes, summarize/translate/PII/categorize/sentiment/image-analysis, CV click/type/get-text. +- Infrastructure: Database, FTP, Terminal/Mainframe, Cryptography. + +## Refactor and Remove Rules + +Use these rules before adding new activities: + +1. Prefer aliases over duplicates. + Example: keep `set_variable` as runtime node; expose `Assign` as UX label/alias. +2. Keep one canonical action per behavior. + Do not add multiple generic "Delete" nodes across packs; scope by domain (`file_delete`, `ftp_delete`). +3. Require a clear runtime owner. + Every new activity must map to runner implementation, validation schema, and demo coverage. +4. Add packs, not isolated one-offs. + New nodes should land in documented pack groups with phase and dependency notes. + +## Definition of Done for New Activities + +- Listed in `apps/server/src/lib/activities.ts` with `pack`, `phase`, `status`. +- Exposed in `/api/activities` and reflected in UI roadmap chips. +- Covered by validation + tests (`apps/server/src/lib/activities.test.ts`). +- Documented in `CHANGELOG.md` and demo/onboarding docs when user-visible. diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 7c34e06..8d613f6 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -56,6 +56,7 @@ Base URL (local): `http://localhost:8080` | Method | Path | Permission | Description | |---|---|---|---| | GET | `/api/templates` | `templates:read` | List built-in workflow templates | +| GET | `/api/templates/:templateId` | `templates:read` | Fetch full template definition + setup guide | | GET | `/api/activities` | `templates:read` | List activity catalog (available + planned packs) | | POST | `/api/autopilot/plan` | `workflows:write` | Generate workflow draft definition from natural-language prompt | | POST | `/api/workflows/from-template` | `workflows:write` | Create workflow from template | @@ -67,6 +68,11 @@ Built-in production starter template IDs: - `email-triage-ticket-create` - `scheduled-health-check-alert` +Activity catalog response also includes: +- `byPhase`, `byPack`: coverage counters for roadmap planning +- `roadmap[]`: ordered pack rollout view (`id`, `label`, `phase`, availability) +- `phaseFocus`: current/next/later implementation focus + ## 6. Document Intelligence and Orchestrator | Method | Path | Permission | Description | diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cda5792..9f3a4f4 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -71,6 +71,7 @@ cd apps/web && npm test && npm run build - Required validation: ```bash cd apps/server && npm test && npm run build +cd apps/server && npm run test:templates ``` ### C. Docs/Community Changes @@ -88,12 +89,14 @@ Use these templates when applicable: ## 6. Testing and CI CI in `.github/workflows/ci.yml` runs on PRs and `main` pushes: - `Server Test and Build` +- `Template quality gate` - `Web Test and Build` - `Web E2E Smoke` Recommended full local validation before review: ```bash cd apps/server && npm test && npm run build +cd apps/server && npm run test:templates cd apps/web && npm test && npm run build ``` diff --git a/docs/DEMOS.md b/docs/DEMOS.md index e767361..264216a 100644 --- a/docs/DEMOS.md +++ b/docs/DEMOS.md @@ -95,6 +95,20 @@ Expected outcome: - Key automation patterns (ingest, transform, branch, sync, alert) are prewired. - Contributors can extend templates without changing core runtime behavior. +## Demo 7: Setup Wizard + Starter Walkthrough +Goal: Show first-time user guidance from template selection to run readiness. + +1. Open `Templates` and select any starter template. +2. Complete required fields in `Template Setup Wizard`. +3. Click `Validate Setup` and confirm checks show `PASS`. +4. Click `Copy Sample Input` and inspect JSON payload. +5. Start `Starter Walkthrough` and progress through all steps. + +Expected outcome: +- Setup friction is reduced with explicit required fields/checks. +- Users can verify run readiness before creating workflow. +- New users get an in-product path for template -> test -> publish. + ## Demo Assets - Template source: `apps/server/src/lib/templates.ts` - Node catalog: `apps/web/src/lib/nodeCatalog.ts` @@ -107,5 +121,6 @@ Expected outcome: - Demo 4: 4-6 minutes - Demo 5: 4-6 minutes - Demo 6: 8-10 minutes +- Demo 7: 4-5 minutes -Total: 31-40 minutes for a full product walkthrough. +Total: 35-45 minutes for a full product walkthrough. diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index ee75e5a..0f7e489 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -25,6 +25,10 @@ Use the guided tutorial and starter workflow: - `docs/tutorials/FIRST_AUTOMATION_10_MIN.md` - `docs/examples/workflows/first-automation.workflow.json` +In-app guidance: +- Use `Starter Walkthrough` in the sidebar for the guided template -> setup -> test -> publish flow. +- Use `Template Setup Wizard` inside `Templates` before creating from starter packs. + This gives you a known-good baseline before deeper changes. ## 4. Choose a Contribution Track diff --git a/docs/README.md b/docs/README.md index 261ceb6..133e883 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ This folder contains the long-form technical documentation for operators and con - [Contributing Guide](./CONTRIBUTING.md): development setup, testing workflow, coding standards, and contribution process. - [Demo Guide](./DEMOS.md): repeatable walkthroughs for product capability demos. - [Starter Templates](../apps/server/src/lib/templates.ts): built-in production workflow baselines. +- [Activity Pack Roadmap](./ACTIVITY_PACK_ROADMAP.md): phased activity expansion and refactor/remove policy. - [Contributor Onboarding](./ONBOARDING.md): fast path for first-time contributors. - [First Automation Tutorial](./tutorials/FIRST_AUTOMATION_10_MIN.md): a 10-minute path from setup to successful test run. - [Starter Workflow Example](./examples/workflows/first-automation.workflow.json): importable workflow file used in onboarding.