From d68de78c9ac682291c51c4916e4b65e856585d0d Mon Sep 17 00:00:00 2001 From: enteresanlikk <23117378+enteresanlikk@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:54:29 +0300 Subject: [PATCH 1/2] feat(flow): move annotation into the node catalog and fix canvas view --- .claude/skills/node-create/SKILL.md | 1 + .../internal/nodeengine/README.md | 4 +- .../internal/nodeengine/domain/nodes/node.go | 6 ++ .../nodeengine/domain/nodes/node_kind.go | 12 +++ .../nodes/airtable/createrecord/node.go | 1 + .../nodes/airtable/listrecords/node.go | 1 + .../nodeengine/nodes/anthropic/chat/node.go | 1 + .../nodeengine/nodes/chatgpt/chat/node.go | 1 + .../nodes/coingecko/airdroptracker/node.go | 1 + .../nodes/coingecko/pricemonitor/node.go | 1 + .../nodeengine/nodes/deepl/translate/node.go | 1 + .../nodeengine/nodes/deepseek/chat/node.go | 1 + .../nodes/discord/sendmedia/node.go | 1 + .../nodes/discord/sendmessage/node.go | 1 + .../nodes/elevenlabs/texttospeech/node.go | 1 + .../nodes/facebook/publishmediapost/node.go | 1 + .../nodes/facebook/publishpost/node.go | 1 + .../nodes/facebook/publishstory/node.go | 1 + .../nodeengine/nodes/gemini/chat/node.go | 1 + .../nodes/gemini/nanobanana/node.go | 1 + .../nodes/google/docs/createorupdate/node.go | 1 + .../nodes/google/docs/readdocs/node.go | 1 + .../nodes/google/drive/createfolder/node.go | 1 + .../nodes/google/drive/createtextfile/node.go | 1 + .../nodes/google/drive/getfile/node.go | 1 + .../nodes/google/drive/uploadfile/node.go | 1 + .../nodes/google/gmail/organizeemails/node.go | 1 + .../nodes/google/gmail/searchemails/node.go | 1 + .../nodes/google/gmail/sendemail/node.go | 1 + .../nodes/google/keep/createnote/node.go | 1 + .../nodes/google/keep/deletenote/node.go | 1 + .../nodes/google/keep/getnote/node.go | 1 + .../nodes/google/keep/listnotes/node.go | 1 + .../sheets/adddatatospreadsheet/node.go | 1 + .../google/sheets/createspreadsheet/node.go | 1 + .../google/sheets/deletespreadsheet/node.go | 1 + .../google/sheets/readspreadsheet/node.go | 1 + .../nodes/google/sheets/update/node.go | 1 + .../nodes/google/youtube/uploadvideo/node.go | 1 + .../nodes/instagram/publishpost/node.go | 1 + .../nodes/instagram/publishreels/node.go | 1 + .../nodes/instagram/publishstory/node.go | 1 + .../nodes/linkedin/publishmediapost/node.go | 1 + .../nodes/linkedin/publishpost/node.go | 1 + .../nodes/notion/createpage/node.go | 1 + .../nodeengine/nodes/notion/getpage/node.go | 1 + .../nodes/notion/searchpages/node.go | 1 + .../nodes/notion/updatepage/node.go | 1 + .../nodeengine/nodes/piapi/audiogen/node.go | 1 + .../nodeengine/nodes/piapi/imagegen/node.go | 1 + .../nodeengine/nodes/piapi/videogen/node.go | 1 + .../nodes/sendgrid/sendemail/node.go | 1 + .../nodeengine/nodes/slack/sendmedia/node.go | 1 + .../nodes/slack/sendmessage/node.go | 1 + .../nodes/soundcloud/createplaylist/node.go | 1 + .../nodes/soundcloud/createtrack/node.go | 1 + .../nodes/sunomusic/createlyrics/node.go | 1 + .../nodes/sunomusic/createmusic/node.go | 1 + .../nodes/system/annotation/node.go | 50 +++++++++++ .../nodeengine/nodes/system/condition/node.go | 1 + .../nodeengine/nodes/system/register.go | 6 ++ .../nodeengine/nodes/system/sleep/node.go | 1 + .../nodeengine/nodes/system/starter/node.go | 1 + .../nodes/telegram/sendmedia/node.go | 1 + .../nodes/telegram/sendmessage/node.go | 1 + .../nodes/tiktok/publishpost/node.go | 1 + .../internal/nodeengine/nodes/veo/node.go | 1 + .../nodes/whatsapp/sendmedia/node.go | 1 + .../nodes/whatsapp/sendtemplate/node.go | 1 + .../nodes/whatsapp/sendtextmessage/node.go | 1 + .../nodes/x/publishmediapost/node.go | 1 + .../nodeengine/nodes/x/publishpost/node.go | 1 + .../taskrunner/task_execution_coordinator.go | 4 + .../taskrunner/task_lifecycle_manager.go | 5 ++ .../workflow-generation-system-instruction.md | 17 +++- apps/platform/public/locales/en/ui.json | 9 +- .../flow-editor/components/flow-canvas.tsx | 75 ++++++++++++---- .../flow-editor/components/flow-sidebar.tsx | 25 ------ .../flow-editor/components/flow-toolbar.tsx | 4 +- .../components/flow-view-controls.tsx | 80 +++++++++++++++++ .../flow-editor/hooks/use-flow-drag.ts | 87 +++++-------------- .../flow-editor/hooks/use-flow-nodes.ts | 1 + .../flow-editor/hooks/use-flow-save.ts | 27 ++---- .../features/flow-editor/nodes/core-node.tsx | 40 ++++----- .../src/features/flow-editor/types.ts | 2 + apps/platform/src/styles/react-flow.css | 8 -- packages/go-packages/dag/node.go | 5 +- 87 files changed, 374 insertions(+), 160 deletions(-) create mode 100644 apps/platform-api/internal/nodeengine/domain/nodes/node_kind.go create mode 100644 apps/platform-api/internal/nodeengine/nodes/system/annotation/node.go create mode 100644 apps/platform/src/features/flow-editor/components/flow-view-controls.tsx diff --git a/.claude/skills/node-create/SKILL.md b/.claude/skills/node-create/SKILL.md index 259089f..230bc33 100644 --- a/.claude/skills/node-create/SKILL.md +++ b/.claude/skills/node-create/SKILL.md @@ -185,6 +185,7 @@ func NewNode(nodeID string) *Node { return &Node{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "", Description: "", diff --git a/apps/platform-api/internal/nodeengine/README.md b/apps/platform-api/internal/nodeengine/README.md index b7d3e6e..e1c9e3b 100644 --- a/apps/platform-api/internal/nodeengine/README.md +++ b/apps/platform-api/internal/nodeengine/README.md @@ -8,9 +8,9 @@ The node engine owns the **definitions** of everything a workflow can do: each n ## Core concepts -- **Node** (`domain/nodes/node.go`): a declarative action descriptor — `ID`, `Version`, name/description, `Icon` (a `Brand` naming the provider's mark plus a `Glyph` naming the action badge), `Inputs`/`Outputs` (the connection points, each a `Key` and an optional `Label`; every node states its own, there is no default), `InputSchema`/`OutputSchema` (`jsonschema-go`), `Categories`/`SubCategories`/`Tags`, `SupportedCredentials`, `Annotations`, `HasNaturalLanguage`, `Disabled`. Implements `NodeManager` (getter interface). +- **Node** (`domain/nodes/node.go`): a declarative action descriptor — `ID`, `Kind` (`action` for everything the runner executes, `note` for canvas-only nodes; every node states its own, there is no default), `Version`, name/description, `Icon` (a `Brand` naming the provider's mark plus a `Glyph` naming the action badge), `Inputs`/`Outputs` (the connection points, each a `Key` and an optional `Label`; every node states its own, there is no default), `InputSchema`/`OutputSchema` (`jsonschema-go`), `Categories`/`SubCategories`/`Tags`, `SupportedCredentials`, `Annotations`, `HasNaturalLanguage`, `Disabled`. Implements `NodeManager` (getter interface). - A node describes how it looks and how it connects, so the UI derives nothing: the artwork lives under `apps/platform/public/assets/icons/{brands,glyphs}/` and is composed at render time. `system_starter` declares an empty `Inputs` slice (no way in), `system_condition` declares two outputs labelled True and False. Credentials (`CredentialIcon`) and MCP servers (`ServerIcon`) name a `Brand` only — the glyph badge belongs to individual nodes. + A node describes how it looks and how it connects, so the UI derives nothing: the artwork lives under `apps/platform/public/assets/icons/{brands,glyphs}/` and is composed at render time. `system_starter` declares an empty `Inputs` slice (no way in), `system_condition` declares two outputs labelled True and False. `system_annotation` is the one `note` node: it is registered in the node catalog so both the UI and the flow generator can offer it, but it has no executor and is absent from the MCP `Tools` slice; `taskrunner.RunnableNodes` drops it before the DAG is built, so it never gets a node execution. Credentials (`CredentialIcon`) and MCP servers (`ServerIcon`) name a `Brand` only — the glyph badge belongs to individual nodes. - **Executor** (`domain/executors/executor.go`): the behavior for a node, keyed by the same ID. `ExecutorManager.ExecuteWithContext(ctx, credentials, data)` is the execution entry point. - **Credential** (`domain/credentials/credential.go`): a provider auth descriptor — schema, `IsOAuth1`/`IsOAuth2`/`IsSupportPlatform`, `SupportedNodes`. OAuth credentials may implement `RefreshableCredential`. `IsSupportPlatform` is auto-derived at load (host-provided OAuth apps). - **Adapter** (`domain/adapters/adapter.go`): a trigger adapter that maps a raw webhook payload into a `TriggerContext` (`source`/`sender`/`prompt`/`payload`, exposed as `$trigger.*` variables). diff --git a/apps/platform-api/internal/nodeengine/domain/nodes/node.go b/apps/platform-api/internal/nodeengine/domain/nodes/node.go index 3c53509..3232932 100644 --- a/apps/platform-api/internal/nodeengine/domain/nodes/node.go +++ b/apps/platform-api/internal/nodeengine/domain/nodes/node.go @@ -6,6 +6,7 @@ import ( type Node struct { ID string `json:"id,omitempty"` + Kind NodeKind `json:"kind"` Version string `json:"version,omitempty"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` @@ -25,6 +26,7 @@ type Node struct { type NodeManager interface { GetID() string + GetKind() NodeKind GetVersion() string GetName() string GetDescription() string @@ -48,6 +50,10 @@ func (n *Node) GetID() string { return n.ID } +func (n *Node) GetKind() NodeKind { + return n.Kind +} + func (n *Node) GetVersion() string { return n.Version } diff --git a/apps/platform-api/internal/nodeengine/domain/nodes/node_kind.go b/apps/platform-api/internal/nodeengine/domain/nodes/node_kind.go new file mode 100644 index 0000000..8de673e --- /dev/null +++ b/apps/platform-api/internal/nodeengine/domain/nodes/node_kind.go @@ -0,0 +1,12 @@ +package nodes + +type NodeKind string + +const ( + NodeKindAction NodeKind = "action" + NodeKindNote NodeKind = "note" +) + +func (k NodeKind) String() string { + return string(k) +} diff --git a/apps/platform-api/internal/nodeengine/nodes/airtable/createrecord/node.go b/apps/platform-api/internal/nodeengine/nodes/airtable/createrecord/node.go index bbbf284..b0ede67 100644 --- a/apps/platform-api/internal/nodeengine/nodes/airtable/createrecord/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/airtable/createrecord/node.go @@ -13,6 +13,7 @@ func NewAirtableCreateRecordNode(nodeID string) *AirtableCreateRecordNode { return &AirtableCreateRecordNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Airtable Create Record", Description: "Create a new record in an Airtable base.", diff --git a/apps/platform-api/internal/nodeengine/nodes/airtable/listrecords/node.go b/apps/platform-api/internal/nodeengine/nodes/airtable/listrecords/node.go index c90ea98..044a057 100644 --- a/apps/platform-api/internal/nodeengine/nodes/airtable/listrecords/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/airtable/listrecords/node.go @@ -14,6 +14,7 @@ func NewAirtableListRecordsNode(nodeID string) *AirtableListRecordsNode { return &AirtableListRecordsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Airtable List Records", Description: "List records from an Airtable base.", diff --git a/apps/platform-api/internal/nodeengine/nodes/anthropic/chat/node.go b/apps/platform-api/internal/nodeengine/nodes/anthropic/chat/node.go index 31e4947..573a820 100644 --- a/apps/platform-api/internal/nodeengine/nodes/anthropic/chat/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/anthropic/chat/node.go @@ -14,6 +14,7 @@ func NewAnthropicChatNode(nodeID string) *AnthropicChatNode { return &AnthropicChatNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Anthropic Chat", Description: "Generate a chat completion using Anthropic Claude.", diff --git a/apps/platform-api/internal/nodeengine/nodes/chatgpt/chat/node.go b/apps/platform-api/internal/nodeengine/nodes/chatgpt/chat/node.go index 8a9abde..5860625 100644 --- a/apps/platform-api/internal/nodeengine/nodes/chatgpt/chat/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/chatgpt/chat/node.go @@ -14,6 +14,7 @@ func NewChatgptChatNode(nodeID string) *ChatgptChatNode { return &ChatgptChatNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "ChatGPT Chat", Description: "Generate a chat completion using OpenAI ChatGPT.", diff --git a/apps/platform-api/internal/nodeengine/nodes/coingecko/airdroptracker/node.go b/apps/platform-api/internal/nodeengine/nodes/coingecko/airdroptracker/node.go index 612a04e..73647af 100644 --- a/apps/platform-api/internal/nodeengine/nodes/coingecko/airdroptracker/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/coingecko/airdroptracker/node.go @@ -14,6 +14,7 @@ func NewCoingeckoAirdropTrackerNode(nodeID string) *CoingeckoAirdropTrackerNode return &CoingeckoAirdropTrackerNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Coingecko Airdrop Tracker", Description: "Track cryptocurrency airdrops via CoinGecko.", diff --git a/apps/platform-api/internal/nodeengine/nodes/coingecko/pricemonitor/node.go b/apps/platform-api/internal/nodeengine/nodes/coingecko/pricemonitor/node.go index 1633abf..11b61d6 100644 --- a/apps/platform-api/internal/nodeengine/nodes/coingecko/pricemonitor/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/coingecko/pricemonitor/node.go @@ -14,6 +14,7 @@ func NewCoingeckoPriceMonitorNode(nodeID string) *CoingeckoPriceMonitorNode { return &CoingeckoPriceMonitorNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Coingecko Price Monitor", Description: "Monitor cryptocurrency prices via CoinGecko.", diff --git a/apps/platform-api/internal/nodeengine/nodes/deepl/translate/node.go b/apps/platform-api/internal/nodeengine/nodes/deepl/translate/node.go index 3a456b4..75e7121 100644 --- a/apps/platform-api/internal/nodeengine/nodes/deepl/translate/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/deepl/translate/node.go @@ -14,6 +14,7 @@ func NewDeeplTranslateNode(nodeID string) *DeeplTranslateNode { return &DeeplTranslateNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "DeepL Translate", Description: "Translate text using DeepL.", diff --git a/apps/platform-api/internal/nodeengine/nodes/deepseek/chat/node.go b/apps/platform-api/internal/nodeengine/nodes/deepseek/chat/node.go index 2fa7ebc..e1314b8 100644 --- a/apps/platform-api/internal/nodeengine/nodes/deepseek/chat/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/deepseek/chat/node.go @@ -14,6 +14,7 @@ func NewDeepseekChatNode(nodeID string) *DeepseekChatNode { return &DeepseekChatNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "DeepSeek Chat", Description: "Generate a chat completion using DeepSeek.", diff --git a/apps/platform-api/internal/nodeengine/nodes/discord/sendmedia/node.go b/apps/platform-api/internal/nodeengine/nodes/discord/sendmedia/node.go index 556142e..41ef086 100644 --- a/apps/platform-api/internal/nodeengine/nodes/discord/sendmedia/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/discord/sendmedia/node.go @@ -13,6 +13,7 @@ func NewDiscordSendMediaNode(nodeID string) *DiscordSendMediaNode { return &DiscordSendMediaNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Discord Send Media", Description: "Send a media file to a Discord channel.", diff --git a/apps/platform-api/internal/nodeengine/nodes/discord/sendmessage/node.go b/apps/platform-api/internal/nodeengine/nodes/discord/sendmessage/node.go index 38ccb15..849a1a4 100644 --- a/apps/platform-api/internal/nodeengine/nodes/discord/sendmessage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/discord/sendmessage/node.go @@ -13,6 +13,7 @@ func NewDiscordSendMessageNode(nodeID string) *DiscordSendMessageNode { return &DiscordSendMessageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Discord Send Message", Description: "Send a text message to a Discord channel.", diff --git a/apps/platform-api/internal/nodeengine/nodes/elevenlabs/texttospeech/node.go b/apps/platform-api/internal/nodeengine/nodes/elevenlabs/texttospeech/node.go index 59b1a9a..2bae53c 100644 --- a/apps/platform-api/internal/nodeengine/nodes/elevenlabs/texttospeech/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/elevenlabs/texttospeech/node.go @@ -14,6 +14,7 @@ func NewElevenlabsTextToSpeechNode(nodeID string) *ElevenlabsTextToSpeechNode { return &ElevenlabsTextToSpeechNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "ElevenLabs Text To Speech", Description: "Convert text to speech using ElevenLabs.", diff --git a/apps/platform-api/internal/nodeengine/nodes/facebook/publishmediapost/node.go b/apps/platform-api/internal/nodeengine/nodes/facebook/publishmediapost/node.go index 3916eb7..9d42b4a 100644 --- a/apps/platform-api/internal/nodeengine/nodes/facebook/publishmediapost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/facebook/publishmediapost/node.go @@ -13,6 +13,7 @@ func NewFacebookPublishMediaPostNode(nodeID string) *FacebookPublishMediaPostNod return &FacebookPublishMediaPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Facebook Publish Media Post", Description: "Publish a post with media to a Facebook page.", diff --git a/apps/platform-api/internal/nodeengine/nodes/facebook/publishpost/node.go b/apps/platform-api/internal/nodeengine/nodes/facebook/publishpost/node.go index 930a0ba..4160878 100644 --- a/apps/platform-api/internal/nodeengine/nodes/facebook/publishpost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/facebook/publishpost/node.go @@ -13,6 +13,7 @@ func NewFacebookPublishPostNode(nodeID string) *FacebookPublishPostNode { return &FacebookPublishPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Facebook Publish Post", Description: "Publish a text post to a Facebook page.", diff --git a/apps/platform-api/internal/nodeengine/nodes/facebook/publishstory/node.go b/apps/platform-api/internal/nodeengine/nodes/facebook/publishstory/node.go index 88bd1b8..b7e74c0 100644 --- a/apps/platform-api/internal/nodeengine/nodes/facebook/publishstory/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/facebook/publishstory/node.go @@ -13,6 +13,7 @@ func NewFacebookPublishStoryNode(nodeID string) *FacebookPublishStoryNode { return &FacebookPublishStoryNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Facebook Publish Story", Description: "Publish a story to a Facebook page.", diff --git a/apps/platform-api/internal/nodeengine/nodes/gemini/chat/node.go b/apps/platform-api/internal/nodeengine/nodes/gemini/chat/node.go index b55da49..80ab290 100644 --- a/apps/platform-api/internal/nodeengine/nodes/gemini/chat/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/gemini/chat/node.go @@ -14,6 +14,7 @@ func NewGeminiChatNode(nodeID string) *GeminiChatNode { return &GeminiChatNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Gemini Chat", Description: "Generate a chat completion using Google Gemini.", diff --git a/apps/platform-api/internal/nodeengine/nodes/gemini/nanobanana/node.go b/apps/platform-api/internal/nodeengine/nodes/gemini/nanobanana/node.go index f127e25..4a13ba7 100644 --- a/apps/platform-api/internal/nodeengine/nodes/gemini/nanobanana/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/gemini/nanobanana/node.go @@ -14,6 +14,7 @@ func NewGeminiImageGenerationNode(nodeID string) *GeminiImageGenerationNode { return &GeminiImageGenerationNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Gemini Nano Banana", Description: "Generate an image from a text prompt using Google Gemini Nano Banana.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/docs/createorupdate/node.go b/apps/platform-api/internal/nodeengine/nodes/google/docs/createorupdate/node.go index 9f3bb61..1e91f0e 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/docs/createorupdate/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/docs/createorupdate/node.go @@ -13,6 +13,7 @@ func NewGoogleDocsCreateOrUpdateNode(nodeID string) *GoogleDocsCreateOrUpdateNod return &GoogleDocsCreateOrUpdateNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Docs Create or Update", Description: "Create a new Google Docs document or update an existing one with the provided content.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/docs/readdocs/node.go b/apps/platform-api/internal/nodeengine/nodes/google/docs/readdocs/node.go index 07ba573..9091de3 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/docs/readdocs/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/docs/readdocs/node.go @@ -13,6 +13,7 @@ func NewGoogleDocsReadDocsNode(nodeID string) *GoogleDocsReadDocsNode { return &GoogleDocsReadDocsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Docs Read", Description: "Read the plain text content of a Google Docs document.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/drive/createfolder/node.go b/apps/platform-api/internal/nodeengine/nodes/google/drive/createfolder/node.go index aef5fc5..ba1c433 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/drive/createfolder/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/drive/createfolder/node.go @@ -14,6 +14,7 @@ func NewGoogleDriveCreateFolderNode(nodeID string) *GoogleDriveCreateFolderNode return &GoogleDriveCreateFolderNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Drive Create Folder", Description: "Create a new folder in Google Drive.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/drive/createtextfile/node.go b/apps/platform-api/internal/nodeengine/nodes/google/drive/createtextfile/node.go index e65bd91..19541cc 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/drive/createtextfile/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/drive/createtextfile/node.go @@ -14,6 +14,7 @@ func NewGoogleDriveCreateTextFileNode(nodeID string) *GoogleDriveCreateTextFileN return &GoogleDriveCreateTextFileNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Drive Create Text File", Description: "Create a plain text file in Google Drive.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/drive/getfile/node.go b/apps/platform-api/internal/nodeengine/nodes/google/drive/getfile/node.go index 1fe071e..a35e423 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/drive/getfile/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/drive/getfile/node.go @@ -13,6 +13,7 @@ func NewGoogleDriveGetFileNode(nodeID string) *GoogleDriveGetFileNode { return &GoogleDriveGetFileNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Drive Get File", Description: "Retrieve a file from Google Drive along with its metadata.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/drive/uploadfile/node.go b/apps/platform-api/internal/nodeengine/nodes/google/drive/uploadfile/node.go index c871486..ef601b2 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/drive/uploadfile/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/drive/uploadfile/node.go @@ -14,6 +14,7 @@ func NewGoogleDriveUploadFileNode(nodeID string) *GoogleDriveUploadFileNode { return &GoogleDriveUploadFileNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Drive Upload File", Description: "Upload one or more files to Google Drive from a list of source URLs.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/gmail/organizeemails/node.go b/apps/platform-api/internal/nodeengine/nodes/google/gmail/organizeemails/node.go index ef01efe..34bffff 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/gmail/organizeemails/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/gmail/organizeemails/node.go @@ -13,6 +13,7 @@ func NewGmailOrganizeEmailsNode(nodeID string) *GmailOrganizeEmailsNode { return &GmailOrganizeEmailsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Gmail Organize Emails", Description: "Organize Gmail messages by applying labels, stars, categories, read state, or trash actions to messages matching a search query.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/gmail/searchemails/node.go b/apps/platform-api/internal/nodeengine/nodes/google/gmail/searchemails/node.go index ee1b196..fb1a5a9 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/gmail/searchemails/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/gmail/searchemails/node.go @@ -13,6 +13,7 @@ func NewGmailSearchEmailsNode(nodeID string) *GmailSearchEmailsNode { return &GmailSearchEmailsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Gmail Search Emails", Description: "Search Gmail messages using Gmail's standard search syntax and return matching email summaries.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/gmail/sendemail/node.go b/apps/platform-api/internal/nodeengine/nodes/google/gmail/sendemail/node.go index 2d34787..6b68a6b 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/gmail/sendemail/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/gmail/sendemail/node.go @@ -13,6 +13,7 @@ func NewGmailSendEmailNode(nodeID string) *GmailSendEmailNode { return &GmailSendEmailNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Gmail Send Email", Description: "Send an email through Gmail.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/keep/createnote/node.go b/apps/platform-api/internal/nodeengine/nodes/google/keep/createnote/node.go index 160ef6e..0f08a8d 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/keep/createnote/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/keep/createnote/node.go @@ -14,6 +14,7 @@ func NewGoogleKeepCreateNoteNode(nodeID string) *GoogleKeepCreateNoteNode { return &GoogleKeepCreateNoteNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Keep Create Note", Description: "Create a new note in Google Keep.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/keep/deletenote/node.go b/apps/platform-api/internal/nodeengine/nodes/google/keep/deletenote/node.go index 77e261d..aed7ba3 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/keep/deletenote/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/keep/deletenote/node.go @@ -13,6 +13,7 @@ func NewGoogleKeepDeleteNoteNode(nodeID string) *GoogleKeepDeleteNoteNode { return &GoogleKeepDeleteNoteNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Keep Delete Note", Description: "Delete a note from Google Keep. This removes the note immediately and cannot be undone.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/keep/getnote/node.go b/apps/platform-api/internal/nodeengine/nodes/google/keep/getnote/node.go index d0f965c..ca4118b 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/keep/getnote/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/keep/getnote/node.go @@ -14,6 +14,7 @@ func NewGoogleKeepGetNoteNode(nodeID string) *GoogleKeepGetNoteNode { return &GoogleKeepGetNoteNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Keep Get Note", Description: "Get a single note from Google Keep by its resource name.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/keep/listnotes/node.go b/apps/platform-api/internal/nodeengine/nodes/google/keep/listnotes/node.go index 9aa8d3b..e1c9e12 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/keep/listnotes/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/keep/listnotes/node.go @@ -14,6 +14,7 @@ func NewGoogleKeepListNotesNode(nodeID string) *GoogleKeepListNotesNode { return &GoogleKeepListNotesNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Keep List Notes", Description: "List notes from Google Keep with optional filtering and pagination.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/sheets/adddatatospreadsheet/node.go b/apps/platform-api/internal/nodeengine/nodes/google/sheets/adddatatospreadsheet/node.go index 84d5bf5..170c78a 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/sheets/adddatatospreadsheet/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/sheets/adddatatospreadsheet/node.go @@ -14,6 +14,7 @@ func NewGoogleSheetsAddDataToSpreadsheetNode(nodeID string) *GoogleSheetsAddData return &GoogleSheetsAddDataToSpreadsheetNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Name: "Google Sheets Add Data to Spreadsheet", Version: "0.0.1", Description: "Append rows of data to a Google Sheets spreadsheet.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/sheets/createspreadsheet/node.go b/apps/platform-api/internal/nodeengine/nodes/google/sheets/createspreadsheet/node.go index d15a165..ab8feee 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/sheets/createspreadsheet/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/sheets/createspreadsheet/node.go @@ -13,6 +13,7 @@ func NewGoogleSheetsCreateSpreadsheetNode(nodeID string) *GoogleSheetsCreateSpre return &GoogleSheetsCreateSpreadsheetNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Sheets Create Spreadsheet", Description: "Create a new Google Sheets spreadsheet with the given title.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/sheets/deletespreadsheet/node.go b/apps/platform-api/internal/nodeengine/nodes/google/sheets/deletespreadsheet/node.go index e6771b5..53e03d9 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/sheets/deletespreadsheet/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/sheets/deletespreadsheet/node.go @@ -13,6 +13,7 @@ func NewGoogleSheetsDeleteSpreadsheetNode(nodeID string) *GoogleSheetsDeleteSpre return &GoogleSheetsDeleteSpreadsheetNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Sheets Delete Spreadsheet", Description: "Delete a Google Sheets spreadsheet by its identifier.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/sheets/readspreadsheet/node.go b/apps/platform-api/internal/nodeengine/nodes/google/sheets/readspreadsheet/node.go index bfa1a7d..a8964a1 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/sheets/readspreadsheet/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/sheets/readspreadsheet/node.go @@ -14,6 +14,7 @@ func NewGoogleSheetsReadSpreadsheetNode(nodeID string) *GoogleSheetsReadSpreadsh return &GoogleSheetsReadSpreadsheetNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Sheets Read Spreadsheet", Description: "Read rows from a Google Sheets spreadsheet, optionally filtering and limiting results.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/sheets/update/node.go b/apps/platform-api/internal/nodeengine/nodes/google/sheets/update/node.go index fa0629f..e5f7cc8 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/sheets/update/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/sheets/update/node.go @@ -13,6 +13,7 @@ func NewGoogleSheetsUpdateNode(nodeID string) *GoogleSheetsUpdateNode { return &GoogleSheetsUpdateNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Google Sheets Update Cell", Description: "Update a single cell in a Google Sheets spreadsheet by row number and column.", diff --git a/apps/platform-api/internal/nodeengine/nodes/google/youtube/uploadvideo/node.go b/apps/platform-api/internal/nodeengine/nodes/google/youtube/uploadvideo/node.go index efc3565..23ef452 100644 --- a/apps/platform-api/internal/nodeengine/nodes/google/youtube/uploadvideo/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/google/youtube/uploadvideo/node.go @@ -14,6 +14,7 @@ func NewYouTubeUploadVideoNode(nodeID string) *YouTubeUploadVideoNode { return &YouTubeUploadVideoNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "YouTube Upload Video", Description: "Upload a video file to YouTube with the given metadata and privacy setting.", diff --git a/apps/platform-api/internal/nodeengine/nodes/instagram/publishpost/node.go b/apps/platform-api/internal/nodeengine/nodes/instagram/publishpost/node.go index c3c0c7b..1e23d63 100644 --- a/apps/platform-api/internal/nodeengine/nodes/instagram/publishpost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/instagram/publishpost/node.go @@ -13,6 +13,7 @@ func NewInstagramPublishPostNode(nodeID string) *InstagramPublishPostNode { return &InstagramPublishPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Instagram Publish Post", Description: "Publish a post to an Instagram account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/instagram/publishreels/node.go b/apps/platform-api/internal/nodeengine/nodes/instagram/publishreels/node.go index 0119d3a..62c503a 100644 --- a/apps/platform-api/internal/nodeengine/nodes/instagram/publishreels/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/instagram/publishreels/node.go @@ -14,6 +14,7 @@ func NewInstagramPublishReelsNode(nodeID string) *InstagramPublishReelsNode { return &InstagramPublishReelsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Instagram Publish Reels", Description: "Publish a Reel to an Instagram account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/instagram/publishstory/node.go b/apps/platform-api/internal/nodeengine/nodes/instagram/publishstory/node.go index 6faa0bf..57b61c3 100644 --- a/apps/platform-api/internal/nodeengine/nodes/instagram/publishstory/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/instagram/publishstory/node.go @@ -13,6 +13,7 @@ func NewInstagramPublishStoryNode(nodeID string) *InstagramPublishStoryNode { return &InstagramPublishStoryNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Instagram Publish Story", Description: "Publish a story to an Instagram account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/linkedin/publishmediapost/node.go b/apps/platform-api/internal/nodeengine/nodes/linkedin/publishmediapost/node.go index 0a73013..fe205fa 100644 --- a/apps/platform-api/internal/nodeengine/nodes/linkedin/publishmediapost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/linkedin/publishmediapost/node.go @@ -14,6 +14,7 @@ func NewLinkedinPublishMediaPostNode(nodeID string) *LinkedinPublishMediaPostNod return &LinkedinPublishMediaPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "LinkedIn Publish Media Post", Description: "Publish a post with media to a LinkedIn account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/linkedin/publishpost/node.go b/apps/platform-api/internal/nodeengine/nodes/linkedin/publishpost/node.go index e2cabed..3a193a8 100644 --- a/apps/platform-api/internal/nodeengine/nodes/linkedin/publishpost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/linkedin/publishpost/node.go @@ -14,6 +14,7 @@ func NewLinkedinPublishPostNode(nodeID string) *LinkedinPublishPostNode { return &LinkedinPublishPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "LinkedIn Publish Post", Description: "Publish a post to a LinkedIn account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/notion/createpage/node.go b/apps/platform-api/internal/nodeengine/nodes/notion/createpage/node.go index 1954920..eee4249 100644 --- a/apps/platform-api/internal/nodeengine/nodes/notion/createpage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/notion/createpage/node.go @@ -13,6 +13,7 @@ func NewNotionCreatePageNode(nodeID string) *NotionCreatePageNode { return &NotionCreatePageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Notion Create Page", Description: "Create a new page in a Notion workspace.", diff --git a/apps/platform-api/internal/nodeengine/nodes/notion/getpage/node.go b/apps/platform-api/internal/nodeengine/nodes/notion/getpage/node.go index c51b57d..be6d4e2 100644 --- a/apps/platform-api/internal/nodeengine/nodes/notion/getpage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/notion/getpage/node.go @@ -13,6 +13,7 @@ func NewNotionGetPageNode(nodeID string) *NotionGetPageNode { return &NotionGetPageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Notion Get Page", Description: "Retrieve a page from a Notion workspace.", diff --git a/apps/platform-api/internal/nodeengine/nodes/notion/searchpages/node.go b/apps/platform-api/internal/nodeengine/nodes/notion/searchpages/node.go index ff88778..6873d2d 100644 --- a/apps/platform-api/internal/nodeengine/nodes/notion/searchpages/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/notion/searchpages/node.go @@ -14,6 +14,7 @@ func NewNotionSearchPagesNode(nodeID string) *NotionSearchPagesNode { return &NotionSearchPagesNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Notion Search Pages", Description: "Search for pages in a Notion workspace.", diff --git a/apps/platform-api/internal/nodeengine/nodes/notion/updatepage/node.go b/apps/platform-api/internal/nodeengine/nodes/notion/updatepage/node.go index 5c236d5..2de034a 100644 --- a/apps/platform-api/internal/nodeengine/nodes/notion/updatepage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/notion/updatepage/node.go @@ -13,6 +13,7 @@ func NewNotionUpdatePageNode(nodeID string) *NotionUpdatePageNode { return &NotionUpdatePageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Notion Update Page", Description: "Update an existing page in a Notion workspace.", diff --git a/apps/platform-api/internal/nodeengine/nodes/piapi/audiogen/node.go b/apps/platform-api/internal/nodeengine/nodes/piapi/audiogen/node.go index b4e3dbb..48dcf63 100644 --- a/apps/platform-api/internal/nodeengine/nodes/piapi/audiogen/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/piapi/audiogen/node.go @@ -14,6 +14,7 @@ func NewPiAPIAudioGenNode(nodeID string) *PiAPIAudioGenNode { return &PiAPIAudioGenNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "PiAPI Audio Generation", Description: "Generate audio using PiAPI.", diff --git a/apps/platform-api/internal/nodeengine/nodes/piapi/imagegen/node.go b/apps/platform-api/internal/nodeengine/nodes/piapi/imagegen/node.go index 6d2365d..24aaeaf 100644 --- a/apps/platform-api/internal/nodeengine/nodes/piapi/imagegen/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/piapi/imagegen/node.go @@ -14,6 +14,7 @@ func NewPiAPIImageGenNode(nodeID string) *PiAPIImageGenNode { return &PiAPIImageGenNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "PiAPI Image Generation", Description: "Generate an image using PiAPI.", diff --git a/apps/platform-api/internal/nodeengine/nodes/piapi/videogen/node.go b/apps/platform-api/internal/nodeengine/nodes/piapi/videogen/node.go index 430d7ad..d2d9628 100644 --- a/apps/platform-api/internal/nodeengine/nodes/piapi/videogen/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/piapi/videogen/node.go @@ -14,6 +14,7 @@ func NewPiAPIVideoGenNode(nodeID string) *PiAPIVideoGenNode { return &PiAPIVideoGenNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "PiAPI Video Generation", Description: "Generate a video using PiAPI.", diff --git a/apps/platform-api/internal/nodeengine/nodes/sendgrid/sendemail/node.go b/apps/platform-api/internal/nodeengine/nodes/sendgrid/sendemail/node.go index f3fab1a..98cbf2d 100644 --- a/apps/platform-api/internal/nodeengine/nodes/sendgrid/sendemail/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/sendgrid/sendemail/node.go @@ -13,6 +13,7 @@ func NewSendgridSendEmailNode(nodeID string) *SendgridSendEmailNode { return &SendgridSendEmailNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "SendGrid Send Email", Description: "Send an email through SendGrid.", diff --git a/apps/platform-api/internal/nodeengine/nodes/slack/sendmedia/node.go b/apps/platform-api/internal/nodeengine/nodes/slack/sendmedia/node.go index a6a5fe7..19954d4 100644 --- a/apps/platform-api/internal/nodeengine/nodes/slack/sendmedia/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/slack/sendmedia/node.go @@ -13,6 +13,7 @@ func NewSlackSendMediaNode(nodeID string) *SlackSendMediaNode { return &SlackSendMediaNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Slack Send Media", Description: "Send a media file to a Slack channel.", diff --git a/apps/platform-api/internal/nodeengine/nodes/slack/sendmessage/node.go b/apps/platform-api/internal/nodeengine/nodes/slack/sendmessage/node.go index 7edc676..80e1cbf 100644 --- a/apps/platform-api/internal/nodeengine/nodes/slack/sendmessage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/slack/sendmessage/node.go @@ -13,6 +13,7 @@ func NewSlackSendMessageNode(nodeID string) *SlackSendMessageNode { return &SlackSendMessageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Slack Send Message", Description: "Send a text message to a Slack channel.", diff --git a/apps/platform-api/internal/nodeengine/nodes/soundcloud/createplaylist/node.go b/apps/platform-api/internal/nodeengine/nodes/soundcloud/createplaylist/node.go index f726789..07164b6 100644 --- a/apps/platform-api/internal/nodeengine/nodes/soundcloud/createplaylist/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/soundcloud/createplaylist/node.go @@ -13,6 +13,7 @@ func NewSoundCloudCreatePlaylistNode(nodeID string) *SoundCloudCreatePlaylistNod return &SoundCloudCreatePlaylistNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "SoundCloud Create Playlist", Description: "Create a new playlist on SoundCloud.", diff --git a/apps/platform-api/internal/nodeengine/nodes/soundcloud/createtrack/node.go b/apps/platform-api/internal/nodeengine/nodes/soundcloud/createtrack/node.go index 99e90d0..4f9a885 100644 --- a/apps/platform-api/internal/nodeengine/nodes/soundcloud/createtrack/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/soundcloud/createtrack/node.go @@ -13,6 +13,7 @@ func NewSoundCloudCreateTrackNode(nodeID string) *SoundCloudCreateTrackNode { return &SoundCloudCreateTrackNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "SoundCloud Create Track", Description: "Upload a new track to SoundCloud.", diff --git a/apps/platform-api/internal/nodeengine/nodes/sunomusic/createlyrics/node.go b/apps/platform-api/internal/nodeengine/nodes/sunomusic/createlyrics/node.go index e7578cc..0355d04 100644 --- a/apps/platform-api/internal/nodeengine/nodes/sunomusic/createlyrics/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/sunomusic/createlyrics/node.go @@ -13,6 +13,7 @@ func NewSunoMusicCreateLyricsNode(nodeID string) *SunoMusicCreateLyricsNode { return &SunoMusicCreateLyricsNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Suno Create Lyrics", Description: "Generate song lyrics using Suno.", diff --git a/apps/platform-api/internal/nodeengine/nodes/sunomusic/createmusic/node.go b/apps/platform-api/internal/nodeengine/nodes/sunomusic/createmusic/node.go index 5754cad..f945e7c 100644 --- a/apps/platform-api/internal/nodeengine/nodes/sunomusic/createmusic/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/sunomusic/createmusic/node.go @@ -14,6 +14,7 @@ func NewSunoMusicCreateMusicNode(nodeID string) *SunoMusicCreateMusicNode { return &SunoMusicCreateMusicNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Suno Create Music", Description: "Generate a music track using Suno.", diff --git a/apps/platform-api/internal/nodeengine/nodes/system/annotation/node.go b/apps/platform-api/internal/nodeengine/nodes/system/annotation/node.go new file mode 100644 index 0000000..2bd03ee --- /dev/null +++ b/apps/platform-api/internal/nodeengine/nodes/system/annotation/node.go @@ -0,0 +1,50 @@ +package annotation + +import ( + "github.com/blocknextai/platform-api/internal/nodeengine/domain/nodes" + gjs "github.com/google/jsonschema-go/jsonschema" +) + +type AnnotationNode struct { + nodes.Node +} + +func NewAnnotationNode(nodeID string) *AnnotationNode { + return &AnnotationNode{ + Node: nodes.Node{ + ID: nodeID, + Kind: nodes.NodeKindNote, + Version: "0.0.1", + Name: "Annotation", + Description: "A free-text note pinned to the canvas.", + Icon: nodes.NodeIcon{ + Glyph: "note", + }, + Inputs: []nodes.NodeHandle{}, + Outputs: []nodes.NodeHandle{}, + Categories: []string{"System"}, + SubCategories: []string{"System"}, + Tags: []string{ + "note", + "comment", + "documentation", + }, + InputSchema: &gjs.Schema{ + Type: "object", + Properties: map[string]*gjs.Schema{ + "note": { + Type: "string", + Description: "The text shown on the canvas.", + }, + }, + }, + HasNaturalLanguage: false, + Annotations: nodes.NodeAnnotations{ + ReadOnly: true, + Idempotent: true, + Destructive: new(false), + OpenWorld: new(false), + }, + }, + } +} diff --git a/apps/platform-api/internal/nodeengine/nodes/system/condition/node.go b/apps/platform-api/internal/nodeengine/nodes/system/condition/node.go index 5dfd749..a67f110 100644 --- a/apps/platform-api/internal/nodeengine/nodes/system/condition/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/system/condition/node.go @@ -14,6 +14,7 @@ func NewConditionNode(nodeID string) *ConditionNode { return &ConditionNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Condition", Description: "Branch the workflow based on a conditional expression.", diff --git a/apps/platform-api/internal/nodeengine/nodes/system/register.go b/apps/platform-api/internal/nodeengine/nodes/system/register.go index 588d1bf..3e8b573 100644 --- a/apps/platform-api/internal/nodeengine/nodes/system/register.go +++ b/apps/platform-api/internal/nodeengine/nodes/system/register.go @@ -5,6 +5,7 @@ import ( "github.com/blocknextai/platform-api/internal/nodeengine/domain/executors" "github.com/blocknextai/platform-api/internal/nodeengine/domain/mcp" "github.com/blocknextai/platform-api/internal/nodeengine/domain/nodes" + "github.com/blocknextai/platform-api/internal/nodeengine/nodes/system/annotation" "github.com/blocknextai/platform-api/internal/nodeengine/nodes/system/condition" "github.com/blocknextai/platform-api/internal/nodeengine/nodes/system/sleep" "github.com/blocknextai/platform-api/internal/nodeengine/nodes/system/starter" @@ -36,6 +37,11 @@ func Register() { nodes.RegisterNode(starterNode) executors.RegisterExecutor(starterExecutor) + annotationNodeID := nodeID + "_annotation" + annotationNode := annotation.NewAnnotationNode(annotationNodeID) + + nodes.RegisterNode(annotationNode) + mcp.RegisterServer(&mcp.Server{ ID: nodeID, Name: "System", diff --git a/apps/platform-api/internal/nodeengine/nodes/system/sleep/node.go b/apps/platform-api/internal/nodeengine/nodes/system/sleep/node.go index 7f1c0a8..f148367 100644 --- a/apps/platform-api/internal/nodeengine/nodes/system/sleep/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/system/sleep/node.go @@ -14,6 +14,7 @@ func NewSleepNode(nodeID string) *SleepNode { return &SleepNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Sleep", Description: "Pause the workflow for a specified duration.", diff --git a/apps/platform-api/internal/nodeengine/nodes/system/starter/node.go b/apps/platform-api/internal/nodeengine/nodes/system/starter/node.go index eb97499..5cf755e 100644 --- a/apps/platform-api/internal/nodeengine/nodes/system/starter/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/system/starter/node.go @@ -13,6 +13,7 @@ func NewStarterNode(nodeID string) *StarterNode { return &StarterNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Starter", Description: "Entry point that starts the workflow execution.", diff --git a/apps/platform-api/internal/nodeengine/nodes/telegram/sendmedia/node.go b/apps/platform-api/internal/nodeengine/nodes/telegram/sendmedia/node.go index e4dd68c..00726a5 100644 --- a/apps/platform-api/internal/nodeengine/nodes/telegram/sendmedia/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/telegram/sendmedia/node.go @@ -13,6 +13,7 @@ func NewTelegramSendMediaNode(nodeID string) *TelegramSendMediaNode { return &TelegramSendMediaNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Telegram Send Media", Description: "Send a media file to a Telegram chat.", diff --git a/apps/platform-api/internal/nodeengine/nodes/telegram/sendmessage/node.go b/apps/platform-api/internal/nodeengine/nodes/telegram/sendmessage/node.go index 79400e5..53e819c 100644 --- a/apps/platform-api/internal/nodeengine/nodes/telegram/sendmessage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/telegram/sendmessage/node.go @@ -13,6 +13,7 @@ func NewTelegramSendMessageNode(nodeID string) *TelegramSendMessageNode { return &TelegramSendMessageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Telegram Send Message", Description: "Send a text message to a Telegram chat.", diff --git a/apps/platform-api/internal/nodeengine/nodes/tiktok/publishpost/node.go b/apps/platform-api/internal/nodeengine/nodes/tiktok/publishpost/node.go index d65dbff..46d46f2 100644 --- a/apps/platform-api/internal/nodeengine/nodes/tiktok/publishpost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/tiktok/publishpost/node.go @@ -13,6 +13,7 @@ func NewTiktokPublishPostNode(nodeID string) *TiktokPublishPostNode { return &TiktokPublishPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "TikTok Publish Post", Description: "Publish a video post to a TikTok account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/veo/node.go b/apps/platform-api/internal/nodeengine/nodes/veo/node.go index 9d3dcda..27b72b0 100644 --- a/apps/platform-api/internal/nodeengine/nodes/veo/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/veo/node.go @@ -14,6 +14,7 @@ func NewVeoNode(nodeID string) *VeoNode { return &VeoNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "Veo", Description: "Generate a video from a text prompt using Veo.", diff --git a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendmedia/node.go b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendmedia/node.go index 6b5dbb6..ef1b10a 100644 --- a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendmedia/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendmedia/node.go @@ -13,6 +13,7 @@ func NewWhatsAppSendMediaNode(nodeID string) *WhatsAppSendMediaNode { return &WhatsAppSendMediaNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "WhatsApp Send Media", Description: "Send a media message via WhatsApp.", diff --git a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtemplate/node.go b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtemplate/node.go index 55bb508..10723cf 100644 --- a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtemplate/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtemplate/node.go @@ -14,6 +14,7 @@ func NewWhatsAppSendTemplateNode(nodeID string) *WhatsAppSendTemplateNode { return &WhatsAppSendTemplateNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "WhatsApp Send Template", Description: "Send a template message via WhatsApp.", diff --git a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtextmessage/node.go b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtextmessage/node.go index b3a097e..f554725 100644 --- a/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtextmessage/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/whatsapp/sendtextmessage/node.go @@ -13,6 +13,7 @@ func NewWhatsAppSendTextMessageNode(nodeID string) *WhatsAppSendTextMessageNode return &WhatsAppSendTextMessageNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "WhatsApp Send Text Message", Description: "Send a text message via WhatsApp.", diff --git a/apps/platform-api/internal/nodeengine/nodes/x/publishmediapost/node.go b/apps/platform-api/internal/nodeengine/nodes/x/publishmediapost/node.go index c222446..4497c42 100644 --- a/apps/platform-api/internal/nodeengine/nodes/x/publishmediapost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/x/publishmediapost/node.go @@ -13,6 +13,7 @@ func NewXPublishMediaPostNode(nodeID string) *XPublishMediaPostNode { return &XPublishMediaPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "X Publish Media Post", Description: "Publish a post with media to an X (Twitter) account.", diff --git a/apps/platform-api/internal/nodeengine/nodes/x/publishpost/node.go b/apps/platform-api/internal/nodeengine/nodes/x/publishpost/node.go index 9dc1753..1f9154b 100644 --- a/apps/platform-api/internal/nodeengine/nodes/x/publishpost/node.go +++ b/apps/platform-api/internal/nodeengine/nodes/x/publishpost/node.go @@ -14,6 +14,7 @@ func NewXPublishPostNode(nodeID string) *XPublishPostNode { return &XPublishPostNode{ Node: nodes.Node{ ID: nodeID, + Kind: nodes.NodeKindAction, Version: "0.0.1", Name: "X Publish Post", Description: "Publish a post to an X (Twitter) account.", diff --git a/apps/platform-api/internal/taskrunner/application/taskrunner/task_execution_coordinator.go b/apps/platform-api/internal/taskrunner/application/taskrunner/task_execution_coordinator.go index 2098c72..857bc88 100644 --- a/apps/platform-api/internal/taskrunner/application/taskrunner/task_execution_coordinator.go +++ b/apps/platform-api/internal/taskrunner/application/taskrunner/task_execution_coordinator.go @@ -13,6 +13,7 @@ import ( executionsApplicationNodeExecutions "github.com/blocknextai/platform-api/internal/executions/application/nodeexecutions" executionsApplicationTaskexecutions "github.com/blocknextai/platform-api/internal/executions/application/taskexecutions" "github.com/blocknextai/platform-api/internal/executions/domain/nodeexecutions" + nodeEngineDomainExecutors "github.com/blocknextai/platform-api/internal/nodeengine/domain/executors" taskRunnerDomain "github.com/blocknextai/platform-api/internal/taskrunner/domain" taskRunnerDomainTask "github.com/blocknextai/platform-api/internal/taskrunner/domain/task" taskRunnerDomainTaskRunner "github.com/blocknextai/platform-api/internal/taskrunner/domain/taskrunner" @@ -147,6 +148,9 @@ func (c *taskExecutionCoordinator) executeNode(ctx context.Context, task *taskRu nodeExecutionID, exists := task.NodeExecutionIDMap[node.ID] if !exists { + if _, runnable := nodeEngineDomainExecutors.GetExecutor(node.NodeID); !runnable { + return ErrExecutorNotFound + } return ErrNodeExecutionNotFound } diff --git a/apps/platform-api/internal/taskrunner/application/taskrunner/task_lifecycle_manager.go b/apps/platform-api/internal/taskrunner/application/taskrunner/task_lifecycle_manager.go index 422a09f..ed9e91b 100644 --- a/apps/platform-api/internal/taskrunner/application/taskrunner/task_lifecycle_manager.go +++ b/apps/platform-api/internal/taskrunner/application/taskrunner/task_lifecycle_manager.go @@ -7,6 +7,7 @@ import ( executionsApplicationNodeExecutions "github.com/blocknextai/platform-api/internal/executions/application/nodeexecutions" executionsApplicationTaskexecutions "github.com/blocknextai/platform-api/internal/executions/application/taskexecutions" + nodeEngineDomainExecutors "github.com/blocknextai/platform-api/internal/nodeengine/domain/executors" taskRunnerDomain "github.com/blocknextai/platform-api/internal/taskrunner/domain" taskRunnerDomainTask "github.com/blocknextai/platform-api/internal/taskrunner/domain/task" taskRunnerDomainTaskRunner "github.com/blocknextai/platform-api/internal/taskrunner/domain/taskrunner" @@ -188,6 +189,10 @@ func (m *taskLifecycleManager) CreateNodeExecutions(ctx context.Context, task *t continue } + if _, runnable := nodeEngineDomainExecutors.GetExecutor(node.NodeID); !runnable { + continue + } + nodeKey := BuildNodeKey(node.NodeID, node.ID) if outputs, hasPreviousOutputs := task.PreviousNodeOutputs[nodeKey]; hasPreviousOutputs { diff --git a/apps/platform-api/prompts/workflow-generation-system-instruction.md b/apps/platform-api/prompts/workflow-generation-system-instruction.md index f49c872..cbda0fa 100644 --- a/apps/platform-api/prompts/workflow-generation-system-instruction.md +++ b/apps/platform-api/prompts/workflow-generation-system-instruction.md @@ -70,6 +70,19 @@ Every workflow MUST begin with EXACTLY ONE starter node as the FIRST entry in th The starter node is the entry point. The first executable node(s) MUST have an incoming edge from "0" (e.g. {"id":"xy-edge__0-1","source":"0","target":"1"}). NEVER omit, delete, rename, or move the starter node. Do NOT add "instruction", "parameters", "credentials", "settings", or "data" to it. +=== NOTE NODES (OPTIONAL) === +Every AVAILABLE_NODES entry carries a "kind". "action" is a runnable step and always uses "type": "core" as described above. "note" is a canvas annotation: it never executes, never connects, and exists only to explain the flow to whoever opens it. + +Emit a note node only where a comment earns its place — a non-obvious routing decision, a parameter the user must fill in themselves, a provider limit worth recording. A note that restates what the node already says is noise; leave it out. +{ + "id": "5", + "type": "annotation", + "nodeId": "system_annotation", + "parameters": { "note": "..." }, + "position": {"x": ..., "y": ...} +} +A note node has NO "instruction" and NO "settings", and it appears in NO edge — not as source, not as target. Position it beside the node it comments on, offset from the column the executable nodes run down, so it never sits between two connected steps. + === instruction FIELD === PRIORITY RULE — parameters FIRST, instruction only when natural language is genuinely required. @@ -274,7 +287,7 @@ RIGHT (every consumer binds the upstream output explicitly): The same applies to fan-in (one consumer with multiple incoming edges): the consumer's parameters/instruction must contain a $reference for EACH incoming edge. === AVAILABLE_NODES FORMAT === -Each entry has: id, version, name, description, icon, inputSchema (defines the parameters fields), outputSchema (defines the data references this node emits — see DATA REFERENCES below), categories, subCategories, tags, supportedCredentials, annotations, disabled, hasNaturalLanguage. +Each entry has: id, kind (see NOTE NODES), version, name, description, icon, inputSchema (defines the parameters fields), outputSchema (defines the data references this node emits — see DATA REFERENCES below), categories, subCategories, tags, supportedCredentials, annotations, disabled, hasNaturalLanguage. Use ONLY id as nodeId. Never invent node ids. Skip any node whose disabled flag is true. === VALIDATION CHECKLIST === @@ -296,10 +309,12 @@ Use ONLY id as nodeId. Never invent node ids. Skip any node whose disabled flag [ ] $references / $trigger.* embedded inside instruction prose are escape-quoted (\"$...\"); when they are the entire JSON string value of a parameters field, no extra quoting is added [ ] Webhook reply consumers (telegram/slack/discord/whatsapp) bind a chat/channel identifier from the trigger payload (in parameters or instruction) [ ] Starter node (id="0", type="starter", nodeId="system_starter") is the FIRST entry in "nodes" array, with only id/type/nodeId/position +[ ] Every note node uses type="annotation", carries only id/type/nodeId/parameters.note/position, and appears in no edge [ ] Output wrapped in ```json:workflow block === NEVER DO === - NEVER emit a "data" wrapper around node fields +- NEVER give a note node ("kind": "note") an edge, an "instruction", or a "settings" block, and NEVER emit it as "type": "core" — a note node is always "type": "annotation" - NEVER emit "subline", "runtimeInstruction", "runtimePrompt", or "references" — these are not part of the schema - NEVER emit a "credentials" field at all (not as {}, not as null) — the runtime resolves credentials at execution time - NEVER emit nodes as an object map keyed by id ({"0": {...}, "1": {...}}). Always use a "nodes" array inside the top-level {"nodes": [...], "edges": [...]} wrapper diff --git a/apps/platform/public/locales/en/ui.json b/apps/platform/public/locales/en/ui.json index 00861c2..ee1afd8 100644 --- a/apps/platform/public/locales/en/ui.json +++ b/apps/platform/public/locales/en/ui.json @@ -7,7 +7,6 @@ "ui.text.activate": "Activate", "ui.text.active": "Active", "ui.text.activeSessions": "Active Sessions", - "ui.text.addAnnotationToFlow": "Add annotation to your flow", "ui.text.addCustomName": "Add Custom Name for this Node", "ui.text.addDocuments": "Add Documents (Max 3)", "ui.text.addGalleryImages": "Add Gallery Images (Max 10)", @@ -32,7 +31,6 @@ "ui.text.alreadyPaidVerifyManually": "Already paid? Verify manually", "ui.text.already_have_account": "Already have an account?", "ui.text.amount": "Amount", - "ui.text.annotationNode": "Annotation", "ui.text.annotationPlaceholder": "Annotate your flows any way you'd like.", "ui.text.api": "API", "ui.text.apiKey": "API Key", @@ -347,6 +345,7 @@ "ui.text.files": "Files", "ui.text.finish": "Finish", "ui.text.finished": "Finished", + "ui.text.fitView": "Fit to screen", "ui.text.flashAI": "Flash AI", "ui.text.flashLiteAI": "Flash-Lite AI", "ui.text.flow": "Flow", @@ -456,6 +455,7 @@ "ui.text.liveOnline": "Live", "ui.text.liveReconnecting": "Reconnecting", "ui.text.loading": "Loading", + "ui.text.lockCanvas": "Lock canvas", "ui.text.locked": "Locked", "ui.text.logOut": "Log out", "ui.text.magic_link_consume_failed": "Failed to sign you in with this magic link. It may have expired.", @@ -860,6 +860,7 @@ "ui.text.uiKey": "UI Key", "ui.text.undoCancellation": "Undo Cancellation", "ui.text.unknown": "Unknown", + "ui.text.unlockCanvas": "Unlock canvas", "ui.text.unpin": "Unpin", "ui.text.unsavedChanges": "Unsaved Changes", "ui.text.unsavedChangesDescription": "You have unsaved changes. Are you sure you want to leave?", @@ -944,5 +945,7 @@ "ui.text.yearly": "Annually", "ui.text.yes": "Yes", "ui.text.yesCancel": "Yes, Cancel", - "ui.text.yesRestore": "Yes, Restore" + "ui.text.yesRestore": "Yes, Restore", + "ui.text.zoomIn": "Zoom in", + "ui.text.zoomOut": "Zoom out" } diff --git a/apps/platform/src/features/flow-editor/components/flow-canvas.tsx b/apps/platform/src/features/flow-editor/components/flow-canvas.tsx index 774bc60..6b067c8 100644 --- a/apps/platform/src/features/flow-editor/components/flow-canvas.tsx +++ b/apps/platform/src/features/flow-editor/components/flow-canvas.tsx @@ -9,10 +9,11 @@ import WelcomeTour from '@/features/tour/components/welcome-tour' import { ReactFlow, - Controls, useNodesState, useEdgesState, addEdge, + useReactFlow, + useNodesInitialized, ReactFlowProvider, Background, BackgroundVariant, @@ -40,6 +41,7 @@ import { FlowSaveDialog } from '@/features/flow-editor/components/flow-save-dial import { FlowNavigationGuard } from '@/features/flow-editor/components/flow-navigation-guard' import { FlowDragPreview } from '@/features/flow-editor/components/flow-drag-preview' import { NodeSettingsPanel } from '@/features/flow-editor/components/node-settings-panel' +import { FlowViewControls } from '@/features/flow-editor/components/flow-view-controls' import { FlowApiSheet } from '@/features/flow-editor/components/flow-api-sheet' import { FlowNodesProvider } from '@/features/flow-editor/contexts/flow-nodes-context' @@ -111,6 +113,9 @@ const FlowCanvas = ({ const [nodes, setNodes] = useNodesState(initialFlow ? [] : defaultNodes) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [selectedCategory, setSelectedCategory] = useState(null) + const reactFlow = useReactFlow() + const [locked, setLocked] = useState(false) + const [framed, setFramed] = useState(!initialFlow?.nodes?.length) const [sidebarOpen, setSidebarOpen] = useState(defaultSidebarOpen) const [chatOpen, setChatOpen] = useState(defaultChatOpen) const [apiSheetOpen, setApiSheetOpen] = useState(false) @@ -128,7 +133,6 @@ const FlowCanvas = ({ updateFlowData, saveFlow, runFlow, - deleteLastSaved, preSave, hasUnsavedChanges, setHasUnsavedChanges, @@ -138,8 +142,6 @@ const FlowCanvas = ({ initialFlow, nodes, edges, - setNodes, - setEdges, }) const { @@ -152,14 +154,13 @@ const FlowCanvas = ({ previewMode, }) - const { dragging, previewPos, previewData, startDrag, annotationDrag } = - useFlowDrag({ - apiNodes, - setNodes, - preSave, - getId, - nextStep: () => nextStepRef.current?.(), - }) + const { dragging, previewPos, previewData, startDrag } = useFlowDrag({ + apiNodes, + setNodes, + preSave, + getId, + nextStep: () => nextStepRef.current?.(), + }) // Search handler const handleSearch = useCallback( @@ -209,6 +210,8 @@ const FlowCanvas = ({ icon: apiNode?.icon, inputs: apiNode?.inputs, outputs: apiNode?.outputs, + handleLayout: node.handleLayout, + note: node.parameters?.note, } }, [apiNodes], @@ -298,6 +301,17 @@ const FlowCanvas = ({ idRef.current = initialFlow?.nodes?.length || 1 }, []) + const nodesInitialized = useNodesInitialized() + const framedRef = useRef(false) + useEffect(() => { + if (framedRef.current || !nodesInitialized || nodes.length === 0) { + return + } + framedRef.current = true + reactFlow.fitView({ padding: 0.2 }) + setFramed(true) + }, [nodesInitialized, nodes.length, reactFlow]) + useEffect(() => { if (initialFlow) { setFunctions() @@ -311,6 +325,28 @@ const FlowCanvas = ({ ) }, [initialFlow, apiNodes]) + const revertFlow = useCallback(() => { + if (initialFlow) { + setFunctions() + } else { + setNodes( + defaultNodes.map((n) => ({ + ...n, + data: { ...n.data, ...buildNodeData(n) }, + })), + ) + setEdges([]) + } + setHasUnsavedChanges(false) + }, [ + initialFlow, + setFunctions, + setNodes, + setEdges, + buildNodeData, + setHasUnsavedChanges, + ]) + const applyGeneratedFlow = useCallback( (flow) => { if (!flow || apiNodes.length === 0) { @@ -454,6 +490,10 @@ const FlowCanvas = ({ className={`w-full h-full`} panOnScroll selectionOnDrag + style={{ opacity: framed ? 1 : 0 }} + nodesDraggable={!locked} + nodesConnectable={!locked} + elementsSelectable={!locked} > + } previewMode={previewMode} hasUnsavedChanges={hasUnsavedChanges} - deleteLastSaved={deleteLastSaved} + deleteLastSaved={revertFlow} runFlow={runFlow} setOpen={setOpen} openApiSheet={ @@ -501,7 +547,6 @@ const FlowCanvas = ({ nextStep={nextStepInit} /> )} - { @@ -80,31 +78,9 @@ const FlowSidebar = ({ ) } - const renderAnnotationCard = () => { - return ( -
-
- ) - } - const renderNodeList = () => { if (selectedCategory) { const items = selectedCategory.nodes ?? [] - const hasSystem = items.some((node: any) => node.type === 'system') if (items.length === 0) { return ( @@ -129,7 +105,6 @@ const FlowSidebar = ({ return (
  • {items.map((item: any, j: number) => renderNodeCard(item, j))} - {hasSystem && renderAnnotationCard()}
  • ) } diff --git a/apps/platform/src/features/flow-editor/components/flow-toolbar.tsx b/apps/platform/src/features/flow-editor/components/flow-toolbar.tsx index 244756a..37fa2bd 100644 --- a/apps/platform/src/features/flow-editor/components/flow-toolbar.tsx +++ b/apps/platform/src/features/flow-editor/components/flow-toolbar.tsx @@ -14,6 +14,7 @@ const FlowToolbar = ({ runFlow, setOpen, openApiSheet, + viewControls, }) => { const { t } = useTranslation() @@ -22,7 +23,8 @@ const FlowToolbar = ({ } return ( -
    +
    + {viewControls}
    + {actions.map(({ key, icon: Icon, label, run }) => ( + + + + + {label} + + ))} +
    + ) + }, +) +FlowViewControls.displayName = 'FlowViewControls' + +export { FlowViewControls } diff --git a/apps/platform/src/features/flow-editor/hooks/use-flow-drag.ts b/apps/platform/src/features/flow-editor/hooks/use-flow-drag.ts index 34117f3..26c499c 100644 --- a/apps/platform/src/features/flow-editor/hooks/use-flow-drag.ts +++ b/apps/platform/src/features/flow-editor/hooks/use-flow-drag.ts @@ -16,6 +16,7 @@ interface PreviewPos { interface DragItem { id: string + kind?: string name: string description: string category: string @@ -74,6 +75,26 @@ export function useFlowDrag({ x: e.clientX + 100, y: e.clientY, }) + + if (dragData.current.kind === 'note') { + const noteNode = { + id, + data: { + id, + note: '', + category: dragData.current.category, + }, + parameters: {}, + nodeId: dragData.current.id, + type: 'annotation', + position, + } + setNodes((nds: FlowNode[]) => + preSave(nds.concat(noteNode as FlowNode), true), + ) + return + } + const newNode = { id, data: { @@ -99,46 +120,6 @@ export function useFlowDrag({ nextStep() } - const onMouseUpAnnotation = (e: MouseEvent) => { - setDragging(false) - setPreviewData(null) - - const id = getId() - - const pane = document.querySelector( - '.react-flow__pane.draggable', - ) as HTMLElement - if (pane) { - pane.style.cursor = 'grab' - } - - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUpAnnotation) - - if (!dragData.current) { - return - } - - const position = screenToFlowPosition({ - x: e.clientX + 100, - y: e.clientY, - }) - - const newNode = { - id, - type: 'annotation', - position, - data: { - note: '', - category: 'system', - }, - } - - setNodes((nds: FlowNode[]) => - preSave(nds.concat(newNode as FlowNode), true), - ) - } - const startDrag = (e: React.MouseEvent, id: string) => { const indx = apiNodes.findIndex((node) => node.id === id) const item = apiNodes[indx] @@ -163,36 +144,10 @@ export function useFlowDrag({ document.addEventListener('mouseup', onMouseUp) } - const annotationDrag = (e: React.MouseEvent) => { - e.preventDefault() - - const item = { - id: 'annotation', - name: 'ui.text.annotationNode', - description: '', - category: 'system', - } - setDragging(true) - dragData.current = item - setPreviewData(item) - setPreviewPos({ x: e.clientX, y: e.clientY }) - - const pane = document.querySelector( - '.react-flow__pane.draggable', - ) as HTMLElement - if (pane) { - pane.style.cursor = 'grabbing' - } - - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUpAnnotation) - } - return { dragging, previewPos, previewData, startDrag, - annotationDrag, } } diff --git a/apps/platform/src/features/flow-editor/hooks/use-flow-nodes.ts b/apps/platform/src/features/flow-editor/hooks/use-flow-nodes.ts index 9584fa1..8abe11e 100644 --- a/apps/platform/src/features/flow-editor/hooks/use-flow-nodes.ts +++ b/apps/platform/src/features/flow-editor/hooks/use-flow-nodes.ts @@ -176,6 +176,7 @@ export function useNodeEngineNodes() { () => (data ?? []).map((item) => ({ id: item.id ?? '', + kind: item.kind, name: item.name ?? '', description: item.description ?? '', icon: item.icon, diff --git a/apps/platform/src/features/flow-editor/hooks/use-flow-save.ts b/apps/platform/src/features/flow-editor/hooks/use-flow-save.ts index 4f58dca..0338571 100644 --- a/apps/platform/src/features/flow-editor/hooks/use-flow-save.ts +++ b/apps/platform/src/features/flow-editor/hooks/use-flow-save.ts @@ -6,8 +6,6 @@ import type { FlowEdge, FlowModel, FlowNode, - SetFlowEdges, - SetFlowNodes, } from '@/features/flow-editor/types' interface FlowData { @@ -19,17 +17,9 @@ interface UseFlowSaveOptions { initialFlow: FlowModel | null nodes: FlowNode[] edges: FlowEdge[] - setNodes: SetFlowNodes - setEdges: SetFlowEdges } -export function useFlowSave({ - initialFlow, - nodes, - edges, - setNodes, - setEdges, -}: UseFlowSaveOptions) { +export function useFlowSave({ initialFlow, nodes, edges }: UseFlowSaveOptions) { const { t } = useTranslation() const navigate = useNavigate() const { organizationId } = useParams() @@ -90,7 +80,14 @@ export function useFlowSave({ } const stripNodesForSave = (nodeList: FlowNode[]) => - nodeList.map(({ data: _data, ...rest }) => rest) + nodeList.map(({ data, ...node }) => ({ + ...node, + handleLayout: data?.handleLayout, + parameters: + data?.note === undefined + ? node.parameters + : { ...node.parameters, note: data.note }, + })) const createOrUpdateFlow = async () => { if (autosaveTimeoutRef.current) { @@ -141,11 +138,6 @@ export function useFlowSave({ } } - const deleteLastSaved = () => { - setNodes(initialFlow?.nodes || []) - setEdges(initialFlow?.edges || []) - } - useEffect(() => { return () => { if (autosaveTimeoutRef.current) { @@ -159,7 +151,6 @@ export function useFlowSave({ updateFlowData, saveFlow, runFlow, - deleteLastSaved, preSave, hasUnsavedChanges, setHasUnsavedChanges, diff --git a/apps/platform/src/features/flow-editor/nodes/core-node.tsx b/apps/platform/src/features/flow-editor/nodes/core-node.tsx index d82473c..1395347 100644 --- a/apps/platform/src/features/flow-editor/nodes/core-node.tsx +++ b/apps/platform/src/features/flow-editor/nodes/core-node.tsx @@ -51,7 +51,7 @@ const CoreNode = memo(({ selected, data }) => { const setEdges = useFlowSetEdges() const [nameOpen, setNameOpen] = useState(false) const [name, setName] = useState(data?.title ?? '') - const [position, setPosition] = useState(data?.position || 'l-r') + const [handleLayout, setHandleLayout] = useState(data?.handleLayout || 't-b') const { t } = useTranslation() const resolveIcon = useIconResolver() const { prefs, Icon } = useMemo(() => { @@ -100,16 +100,16 @@ const CoreNode = memo(({ selected, data }) => { if (!data) { return } - if (!data.position || (data.position && data.position !== position)) { - updateField('position', position) + if (data.handleLayout !== handleLayout) { + updateField('handleLayout', handleLayout) } - }, [position]) + }, [handleLayout]) useEffect(() => { if (!data?.id) { return } updateNodeInternals(data.id) - }, [position, updateNodeInternals, data?.id]) + }, [handleLayout, updateNodeInternals, data?.id]) useEffect(() => { if (data?.title !== undefined && data.title !== name) { @@ -124,21 +124,21 @@ const CoreNode = memo(({ selected, data }) => { setPosition('l-r')} + onClick={() => setHandleLayout('l-r')} > {' '} {t('ui.text.right')}{' '} setPosition('l-t')} + onClick={() => setHandleLayout('l-t')} > {' '} {t('ui.text.top')}{' '} setPosition('l-b')} + onClick={() => setHandleLayout('l-b')} > {' '} {' '} @@ -151,14 +151,14 @@ const CoreNode = memo(({ selected, data }) => { setPosition('t-l')} + onClick={() => setHandleLayout('t-l')} > {' '} {t('ui.text.left')}{' '} setPosition('t-b')} + onClick={() => setHandleLayout('t-b')} > {' '} {' '} @@ -166,7 +166,7 @@ const CoreNode = memo(({ selected, data }) => { setPosition('t-r')} + onClick={() => setHandleLayout('t-r')} > {' '} {t('ui.text.right')}{' '} @@ -178,21 +178,21 @@ const CoreNode = memo(({ selected, data }) => { setPosition('b-l')} + onClick={() => setHandleLayout('b-l')} > {' '} {t('ui.text.left')}{' '} setPosition('b-t')} + onClick={() => setHandleLayout('b-t')} > {' '} {t('ui.text.top')}{' '} setPosition('b-r')} + onClick={() => setHandleLayout('b-r')} > {' '} {t('ui.text.right')}{' '} @@ -204,21 +204,21 @@ const CoreNode = memo(({ selected, data }) => { setPosition('r-l')} + onClick={() => setHandleLayout('r-l')} > {' '} {t('ui.text.left')}{' '} setPosition('r-t')} + onClick={() => setHandleLayout('r-t')} > {' '} {t('ui.text.top')}{' '} setPosition('r-b')} + onClick={() => setHandleLayout('r-b')} > {' '} {' '} @@ -228,7 +228,7 @@ const CoreNode = memo(({ selected, data }) => { ) - }, [position, updateField, t]) + }, [handleLayout, updateField, t]) return ( <> @@ -242,7 +242,7 @@ const CoreNode = memo(({ selected, data }) => { @@ -270,7 +270,7 @@ const CoreNode = memo(({ selected, data }) => { type="source" - position={LAYOUT[position][1]} + position={LAYOUT[handleLayout][1]} tooltip={t('ui.text.dataOutput')} /> diff --git a/apps/platform/src/features/flow-editor/types.ts b/apps/platform/src/features/flow-editor/types.ts index 59a8cca..efa5050 100644 --- a/apps/platform/src/features/flow-editor/types.ts +++ b/apps/platform/src/features/flow-editor/types.ts @@ -31,6 +31,7 @@ export interface NodeIcon { export interface NodeEngineNode { id?: string + kind?: string name?: string description?: string icon: NodeIcon @@ -54,6 +55,7 @@ export type IconSource = { brand?: string; glyph?: string } | null | undefined export interface ResolvedNode { id: string + kind?: string name: string description: string icon: IconSource diff --git a/apps/platform/src/styles/react-flow.css b/apps/platform/src/styles/react-flow.css index f89113c..7ec6b37 100644 --- a/apps/platform/src/styles/react-flow.css +++ b/apps/platform/src/styles/react-flow.css @@ -1,11 +1,3 @@ .react-flow__handle { @apply p-1.5 rounded-full !border-none !bg-current; } - -.react-flow__panel { - @apply rounded-md border-zinc-500 shadow-xs overflow-hidden right-0 !left-auto; -} - -.react-flow__attribution { - @apply hidden; -} diff --git a/packages/go-packages/dag/node.go b/packages/go-packages/dag/node.go index 26f6a2f..c66ac93 100644 --- a/packages/go-packages/dag/node.go +++ b/packages/go-packages/dag/node.go @@ -1,7 +1,9 @@ package dag // Node represents a single workflow node in the DAG, including its type, -// instructions, parameters, settings, credentials, and position. +// instructions, parameters, settings, credentials, and canvas placement. +// HandleLayout is canvas-only: it names the sides the node's input and output +// handles sit on, in "-" form ("l-r", "t-b", …). type Node struct { ID string `json:"id"` Type string `json:"type"` @@ -13,4 +15,5 @@ type Node struct { Settings *NodeSettings `json:"settings,omitempty"` Credentials map[string]any `json:"credentials,omitempty"` Position *NodePosition `json:"position,omitempty"` + HandleLayout string `json:"handleLayout,omitempty"` } From c83f2a0d09576a86c8a3246b8d4e5854cf044b3f Mon Sep 17 00:00:00 2001 From: enteresanlikk <23117378+enteresanlikk@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:55:08 +0300 Subject: [PATCH 2/2] fix(compose): make the default self-hosted install durable --- .env.example | 12 +++++++---- README.md | 21 ++++++++++++++++++- .../auth/components/providers/metamask.tsx | 5 ----- apps/platform/src/lib/config.ts | 1 - docker-compose.local.yml | 17 ++++++++------- docker-compose.prod.yml | 17 ++++++++------- docker/file-gateway-api/Dockerfile | 3 +++ docker/platform/env.sh | 1 - 8 files changed, 51 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index 4aca7f1..0ea96ca 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,9 @@ MCP_API_READ_TIMEOUT=120s MCP_API_WRITE_TIMEOUT=120s MCP_API_IDLE_TIMEOUT=180s MCP_API_REDUCE_MEMORY_USAGE=false +# Wildcard on purpose: MCP clients are not browsers and have no fixed origin. +# Every MCP route is behind an API key, so the browser same-origin policy is +# not the control here. Every other service pins its origins. MCP_API_ALLOW_ORIGINS=* MCP_API_ALLOW_HEADERS='Origin, Content-Type, Accept, Authorization, X-API-Key, Mcp-Session-Id, Mcp-Protocol-Version' MCP_API_ALLOW_METHODS=GET,POST,DELETE @@ -346,10 +349,10 @@ FILE_GATEWAY_AUTH_JWT_TOKEN_LEEWAY=0s # Supported drivers: local | s3 | bunny FILE_GATEWAY_STORAGE_DRIVER=local -FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_PATH=/tmp/uploads/public +FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_PATH=/data/uploads/public FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_BASE_URL="${FILE_GATEWAY_API_BASE_URL}/uploads" -FILE_GATEWAY_STORAGE_LOCAL_PRIVATE_PATH=/tmp/uploads/private +FILE_GATEWAY_STORAGE_LOCAL_PRIVATE_PATH=/data/uploads/private FILE_GATEWAY_STORAGE_S3_PUBLIC_HOST=https://cdn.blocknext.ai FILE_GATEWAY_STORAGE_S3_PUBLIC_ACCESS_KEY_ID=local-development-access-key @@ -380,7 +383,8 @@ FILE_GATEWAY_DOWNLOAD_TIMEOUT=30s VITE_PLATFORM_API_URL="${PLATFORM_API_BASE_URL}" VITE_MCP_API_URL="${MCP_API_BASE_URL}" -VITE_WEB3_LOGIN_ENABLED=true - # Docker compose service +# Image tag the stack runs; unset means latest. Pin it before upgrading. +# BLOCKNEXT_VERSION=latest + COMPOSE_PROFILES=cache-${CACHE_TYPE},broker-${BROKER_TYPE},lock-${TASK_RUNNER_LEADER_PROVIDER},semaphore-${TASK_RUNNER_SEMAPHORE_PROVIDER},task-runner-${TASK_RUNNER_MODE} diff --git a/README.md b/README.md index e75b18e..f70a1f3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It is built for end users, not just developers: flows stay simple, readable and ## Highlights - **Visual flow builder** — compose workflows on an intuitive drag-and-drop canvas. Flows are deliberately simple: no loops, no sub-workflows, no expression language to learn. -- **Describe it, don't configure it** — give a node plain-language instructions and an LLM fills in its parameters at run time. Anything you set explicitly always wins over what the model infers. +- **Describe it, don't configure it** — give a node plain-language instructions and an LLM fills in its parameters at run time. Anything you set explicitly always wins over what the model infers. This one and flow generation are the only features that need a key of your own (Gemini); both are off by default. Everything else — the canvas, triggers, the MCP server, every integration node — works on a fresh install. - **Built-in MCP server** — every integration node doubles as an [MCP](https://modelcontextprotocol.io) tool. Point Claude (or any MCP client) at your BlockNext server with an API key and use your connected services from chat. - **AI-powered nodes** — LLMs and generative AI (text, image, audio, video) as first-class building blocks, alongside integrations for the tools you already use. - **Async done right** — long-running AI jobs (video, music generation) are a single node. The node starts the job, polls the provider, and returns the finished result — no wait/check/loop scaffolding on your canvas. @@ -31,10 +31,29 @@ Docker and `make` are all you need — Go and Bun are only required for The UI is served on http://localhost:4000. Run `make help` for every target. +One thing to know on a fresh install: `EMAIL_SENDER_PROVIDER` defaults to `log`, so +every outbound mail is printed instead of sent. Signing up with a password does not +wait for verification — you land in the app straight away — but magic-link sign-in and +password reset need the link, and it is in `make docker-logs`. Point the sender at +SMTP, Resend or SendGrid when you have credentials. + To build and run everything from source instead, see [CONTRIBUTING.md](.github/CONTRIBUTING.md) — the development workflow uses `make local-docker-up`. +## Upgrading + +Set `BLOCKNEXT_VERSION` in `.env` to the release you want (it defaults to `latest`), +then: + +```bash +make docker-pull +make docker-up +``` + +Migrations run in their own container before the services start, so schema changes +are applied for you. + ## Community - [Architecture overview](ARCHITECTURE.md) diff --git a/apps/platform/src/features/auth/components/providers/metamask.tsx b/apps/platform/src/features/auth/components/providers/metamask.tsx index 7f21b76..bb4d514 100644 --- a/apps/platform/src/features/auth/components/providers/metamask.tsx +++ b/apps/platform/src/features/auth/components/providers/metamask.tsx @@ -3,15 +3,10 @@ import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import tokenManager from '@/lib/token-manager' import { ProviderIcon } from '@/features/auth/components/provider-icon' -import { config } from '@/lib/config' const MetaMaskLoginButton = ({ mode = 'login', authActions }) => { const { t } = useTranslation() - if (!config.web3LoginEnabled) { - return null - } - const isInstalled = typeof window.ethereum !== 'undefined' const redirect = () => { diff --git a/apps/platform/src/lib/config.ts b/apps/platform/src/lib/config.ts index 9d042c7..6e2d455 100644 --- a/apps/platform/src/lib/config.ts +++ b/apps/platform/src/lib/config.ts @@ -13,5 +13,4 @@ export const config = { platformApiUrl: env('VITE_PLATFORM_API_URL') || 'VITE_PLATFORM_API_URL is not set', mcpApiUrl: env('VITE_MCP_API_URL') || 'VITE_MCP_API_URL is not set', - web3LoginEnabled: env('VITE_WEB3_LOGIN_ENABLED') === 'true', } diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 0109918..18bbc99 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -51,7 +51,7 @@ services: restart: "no" platform-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -70,7 +70,7 @@ services: - "${PLATFORM_API_PORT:-3000}:${PLATFORM_API_PORT:-3000}" mcp-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -89,7 +89,7 @@ services: - "${MCP_API_PORT:-3100}:${MCP_API_PORT:-3100}" webhook-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -108,7 +108,7 @@ services: - "${WEBHOOK_API_PORT:-3200}:${WEBHOOK_API_PORT:-3200}" task-worker: - restart: on-failure:10 + restart: unless-stopped profiles: - task-runner-queue env_file: @@ -126,7 +126,7 @@ services: BINARY_NAME: task-worker event-relay-worker: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -143,7 +143,7 @@ services: file-gateway-api: container_name: file-gateway-api - restart: on-failure:10 + restart: unless-stopped build: context: . dockerfile: docker/file-gateway-api/Dockerfile @@ -151,10 +151,12 @@ services: - .env ports: - "${FILE_GATEWAY_API_PORT:-3300}:${FILE_GATEWAY_API_PORT:-3300}" + volumes: + - uploads_data:/data/uploads platform: container_name: platform - restart: on-failure:10 + restart: unless-stopped build: context: . dockerfile: docker/platform/nginx.Dockerfile @@ -169,3 +171,4 @@ services: volumes: db_data: task_queue_redis_data: + uploads_data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c73349c..01997c7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -49,7 +49,7 @@ services: restart: "no" platform-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -63,7 +63,7 @@ services: - "${PLATFORM_API_PORT:-3000}:${PLATFORM_API_PORT:-3000}" mcp-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -77,7 +77,7 @@ services: - "${MCP_API_PORT:-3100}:${MCP_API_PORT:-3100}" webhook-api: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -91,7 +91,7 @@ services: - "${WEBHOOK_API_PORT:-3200}:${WEBHOOK_API_PORT:-3200}" task-worker: - restart: on-failure:10 + restart: unless-stopped profiles: - task-runner-queue env_file: @@ -105,7 +105,7 @@ services: image: ghcr.io/blocknextai/task-worker:${BLOCKNEXT_VERSION:-latest} event-relay-worker: - restart: on-failure:10 + restart: unless-stopped env_file: - .env depends_on: @@ -119,16 +119,18 @@ services: file-gateway-api: container_name: file-gateway-api image: ghcr.io/blocknextai/file-gateway-api:${BLOCKNEXT_VERSION:-latest} - restart: on-failure:10 + restart: unless-stopped env_file: - .env ports: - "${FILE_GATEWAY_API_PORT:-3300}:${FILE_GATEWAY_API_PORT:-3300}" + volumes: + - uploads_data:/data/uploads platform: container_name: platform image: ghcr.io/blocknextai/platform:${BLOCKNEXT_VERSION:-latest} - restart: on-failure:10 + restart: unless-stopped env_file: - .env ports: @@ -137,3 +139,4 @@ services: volumes: db_data: task_queue_redis_data: + uploads_data: diff --git a/docker/file-gateway-api/Dockerfile b/docker/file-gateway-api/Dockerfile index 86aded5..9d4e46c 100644 --- a/docker/file-gateway-api/Dockerfile +++ b/docker/file-gateway-api/Dockerfile @@ -26,11 +26,14 @@ RUN --mount=type=cache,target=/go/pkg/mod \ cd apps/file-gateway-api && \ go build -ldflags="-s -w" -trimpath -o /build/file-gateway-api ./cmd/file-gateway-api/main.go +RUN mkdir -p /uploads/public /uploads/private && chown -R 65532:65532 /uploads + FROM gcr.io/distroless/static-debian13:nonroot AS production WORKDIR /app COPY --from=build /build/file-gateway-api . +COPY --from=build --chown=nonroot:nonroot /uploads /data/uploads EXPOSE 3300 diff --git a/docker/platform/env.sh b/docker/platform/env.sh index 8b13da2..cbfaf81 100644 --- a/docker/platform/env.sh +++ b/docker/platform/env.sh @@ -6,6 +6,5 @@ cat > /usr/share/nginx/html/env.js <