* Add workspace webhooks and drop the unused automation webhook node. Give workspaces HMAC-signed outgoing webhooks for the post lifecycle, with retry, auto-pause, replay, and live logs, and keep HTTP Request as the only outbound automation node. * Tighten webhook controller and validation after review. Drop the redundant workspace redirects, prune logs without counting, and validate events/status with Rule::enum. * Move leftover webhook UI copy behind i18n. HTTP status phrases, delete-cancel, and validation attribute names were still English literals. * Build the webhook-paused email through Maizzle. The hand-written Blade skipped the shared layout, header, and footer used by the other mail templates. * Cover real webhook dispatch paths and restyle the webhook pages. * Ask for the shared delete keyword when confirming a webhook delete. The endpoint URL is a poor confirm string; posts and assets already use the common "delete" keyword. * Fix webhook review blockers so CI can go green. Drop leftover French automation keys, stop mutating Inertia log props, and show delivered_at instead of created_at. * Close the remaining webhook review gaps. Keep Echo log updates across infinite scroll, align the channel with the policy, persist log ids across retries, and fail unknown automation nodes without throwing. * Stop webhook delivery after disable and record last sent only on success. Queued jobs now skip paused or disabled endpoints unless the user replays, and changing the URL re-pings it first. * Limit webhooks to owners and admins, and encrypt signing secrets. Members can no longer create or inspect outgoing integrations, and secrets stay encrypted at rest. * Cover webhook secret hiding, skip-ping, and failed-delivery edges. * Send the full post on webhooks after labels and platforms are saved. * Fix webhook payloads for integer media ids and type webhook status. * Split the webhook show page into focused components. * Reset live webhook logs when switching endpoints. * Keep the newest webhook logs at the top after live merges. * Cast media item ids to string without the extra scalar check. * Add post.unscheduled webhooks and put the log id on the envelope. Unscheduling is now a first-class event, and receivers can send the delivery id back so we can find the matching log. * Translate webhook event names in the UI. * Make the webhook show page full-width and stop stacking flash toasts. * Translate remaining webhook UI copy in every locale. * Sign webhook pings and drop author email from the payload. * Send signed webhook tests after create instead of pinging on save. Create and update only block private URLs so the receiver can copy the secret first. The show page then sends a signed webhook.test with an object data envelope. * Polish webhook test UX and always mint the dispatch log id in the job. Keep send-test in the actions menu (its own group) and drop the leftover constructor param so retries reuse the serialized id instead of a caller-supplied one.
402 lines
16 KiB
PHP
402 lines
16 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
return [
|
||
'mobile_notice' => 'The automation editor works best on a larger screen. Open it on a desktop to build your workflow.',
|
||
'title' => 'Automations',
|
||
'default_name' => 'New automation',
|
||
|
||
'actions' => [
|
||
'new' => 'New automation',
|
||
'edit' => 'Edit',
|
||
'save' => 'Save',
|
||
'activate' => 'Activate',
|
||
'pause' => 'Pause',
|
||
'delete' => 'Delete',
|
||
'retry' => 'Retry',
|
||
'guide' => 'Learn how it works',
|
||
],
|
||
|
||
'tabs' => [
|
||
'build' => 'Build',
|
||
'variables' => 'Variables',
|
||
'test' => 'Test',
|
||
],
|
||
|
||
'nav' => [
|
||
'workflow' => 'Workflow',
|
||
'invocations' => 'Invocations',
|
||
'metrics' => 'Metrics',
|
||
'settings' => 'Settings',
|
||
],
|
||
|
||
'settings' => [
|
||
'general' => 'General',
|
||
'general_description' => 'Rename this automation.',
|
||
'name_label' => 'Name',
|
||
'name_saved' => 'Automation renamed.',
|
||
'status_title' => 'Status',
|
||
'status_description' => 'Activate to start running it, or pause to stop.',
|
||
'activated_at' => 'Activated :date',
|
||
'paused_at' => 'Paused :date',
|
||
'created_at' => 'Created :date',
|
||
'danger_title' => 'Danger zone',
|
||
'danger_description' => 'Irreversible actions.',
|
||
'delete_title' => 'Delete this automation',
|
||
'delete_description' => 'Permanently removes the automation and its run history.',
|
||
],
|
||
|
||
'status_run' => [
|
||
'pending' => 'Pending',
|
||
'running' => 'Running',
|
||
'waiting' => 'Waiting',
|
||
'completed' => 'Completed',
|
||
'failed' => 'Failed',
|
||
'cancelled' => 'Cancelled',
|
||
],
|
||
|
||
'node_type' => [
|
||
'trigger' => 'Trigger',
|
||
'generate' => 'Generate content',
|
||
'delay' => 'Delay',
|
||
'condition' => 'Condition',
|
||
'publish' => 'Publish',
|
||
'end' => 'End',
|
||
'fetch_rss' => 'Fetch RSS',
|
||
'http_request' => 'HTTP request',
|
||
],
|
||
|
||
'invocations' => [
|
||
'empty' => 'No invocations yet.',
|
||
'refresh' => 'Refresh',
|
||
'search_placeholder' => 'Search by run ID…',
|
||
'copied' => 'Run ID copied.',
|
||
'loading' => 'Loading steps…',
|
||
'no_steps' => 'No steps recorded.',
|
||
'load_error' => 'Could not load steps.',
|
||
'steps' => '{0}No steps|{1}:count step|[2,*]:count steps',
|
||
'filter' => [
|
||
'all' => 'All statuses',
|
||
],
|
||
'columns' => [
|
||
'timestamp' => 'Timestamp',
|
||
'run' => 'Run',
|
||
'status' => 'Status',
|
||
'message' => 'Last message',
|
||
'duration' => 'Duration',
|
||
],
|
||
'summary' => [
|
||
'completed' => 'Workflow completed',
|
||
'failed' => 'Workflow failed',
|
||
'running' => 'Workflow running',
|
||
'cancelled' => 'Workflow cancelled',
|
||
'pending' => 'Workflow pending',
|
||
],
|
||
],
|
||
|
||
'metrics' => [
|
||
'overview' => 'Overview',
|
||
'runs_over_time' => 'Runs over time',
|
||
'posts_by_platform' => 'Posts by platform',
|
||
'no_posts' => 'No posts published in this period.',
|
||
'cards' => [
|
||
'runs' => 'Total runs',
|
||
'completed' => 'Completed',
|
||
'failed' => 'Failed',
|
||
'in_progress' => 'In progress',
|
||
'success_rate' => 'Success rate',
|
||
'avg_duration' => 'Avg duration',
|
||
'posts_created' => 'Posts created',
|
||
],
|
||
'legend' => [
|
||
'started' => 'Started',
|
||
'completed' => 'Completed',
|
||
'failed' => 'Failed',
|
||
],
|
||
],
|
||
|
||
'categories' => [
|
||
'sources' => 'Sources',
|
||
'content' => 'Content',
|
||
'flow' => 'Flow',
|
||
'output' => 'Output',
|
||
],
|
||
|
||
'variables' => [
|
||
'title' => 'Workflow variables',
|
||
'hint' => 'Reusable values referenced anywhere with {{ variables.KEY }}. Stored encrypted.',
|
||
'empty' => 'No variables yet.',
|
||
'key' => 'Key',
|
||
'value' => 'Value',
|
||
'key_placeholder' => 'API_KEY',
|
||
'value_placeholder' => 'Value',
|
||
'add' => 'New variable',
|
||
],
|
||
|
||
'expr' => [
|
||
'trigger_event' => 'Trigger event name',
|
||
'trigger_fired_at' => 'When the trigger fired',
|
||
'trigger_post_id' => 'Triggering post ID',
|
||
'trigger_post_content' => 'Triggering post content',
|
||
'trigger_post_status' => 'Triggering post status',
|
||
'trigger_post_scheduled_at' => 'When the post is scheduled',
|
||
'trigger_post_published_at' => 'When the post was published',
|
||
'fetched_title' => 'Fetched item title',
|
||
'fetched_link' => 'Fetched item link',
|
||
'fetched_date' => 'Fetched item publish date',
|
||
'fetched_content' => 'Fetched item full content',
|
||
'fetched_description' => 'Fetched item summary',
|
||
'fetched_author' => 'Fetched item author',
|
||
'fetched_image' => 'Fetched item image URL',
|
||
'fetched_categories' => 'Fetched item categories',
|
||
'fetched_enclosure' => 'Fetched item media (audio/video/file)',
|
||
'fetched_pubdate' => 'Fetched item publish date',
|
||
'fetched_http' => 'Fetched HTTP item (append a field)',
|
||
'generated_content' => 'AI-generated post content',
|
||
'generated_post_url' => 'AI-generated post URL',
|
||
'variable' => 'Workflow variable',
|
||
'now' => 'Current date & time',
|
||
],
|
||
|
||
'test' => [
|
||
'description' => 'Runs the automation end-to-end using a synthesized trigger payload. Useful for validating each node without waiting for the real schedule or feed.',
|
||
'starting' => 'Starting test run…',
|
||
'in_progress' => 'In progress',
|
||
'completed' => 'Completed',
|
||
'failed' => 'Failed',
|
||
'waiting' => 'Waiting',
|
||
'close' => 'Close',
|
||
'no_node_runs' => 'Waiting for the first node to start…',
|
||
'node_input' => 'Input',
|
||
'node_output' => 'Output',
|
||
'node_error' => 'Error',
|
||
'no_new_items' => 'No new items — nothing downstream ran.',
|
||
'error_starting' => 'Could not start the test run.',
|
||
'with_real_data' => 'With real data',
|
||
'run' => 'Run test',
|
||
'idle_hint' => 'Hit Run test to execute the automation end-to-end.',
|
||
'real_data_hint' => 'This test will publish posts, advance polling watermarks, and trigger external side effects.',
|
||
'dry_badge' => 'Dry run',
|
||
],
|
||
|
||
'status' => [
|
||
'draft' => 'Draft',
|
||
'active' => 'Active',
|
||
'paused' => 'Paused',
|
||
],
|
||
|
||
'index' => [
|
||
'empty_title' => 'No automations yet',
|
||
'empty_description' => 'Create your first automation to start publishing on autopilot.',
|
||
'columns' => [
|
||
'name' => 'Name',
|
||
'status' => 'Status',
|
||
'created' => 'Created',
|
||
],
|
||
],
|
||
|
||
'form' => [
|
||
'activate_error_fallback' => 'Could not activate automation.',
|
||
'pause_error_fallback' => 'Could not pause automation.',
|
||
'save_error_fallback' => 'Could not save automation.',
|
||
'save_success' => 'Automation saved.',
|
||
'empty_canvas_title' => 'Start building your automation',
|
||
'empty_canvas_description' => 'Drag a node from the left panel to get started.',
|
||
'name_placeholder' => 'Untitled automation',
|
||
],
|
||
|
||
'nodes' => [
|
||
'trigger' => 'Trigger',
|
||
'generate' => 'Generate',
|
||
'delay' => 'Delay',
|
||
'condition' => 'Condition',
|
||
'publish' => 'Publish',
|
||
'end' => 'End',
|
||
'end_summary' => 'Stops the automation here',
|
||
'fetch_rss' => 'Fetch RSS',
|
||
'http_request' => 'HTTP Request',
|
||
'handles' => [
|
||
'items' => 'has items',
|
||
'no_items' => 'no items',
|
||
],
|
||
],
|
||
|
||
'config' => [
|
||
'select_placeholder' => 'Select…',
|
||
'invalid_json' => 'This isn’t valid JSON yet.',
|
||
'expand_editor' => 'Expand editor',
|
||
'minimize_editor' => 'Minimize',
|
||
|
||
'trigger' => [
|
||
'type' => 'Trigger type',
|
||
'types' => [
|
||
'schedule' => 'Schedule',
|
||
'post_published' => 'When a post is published',
|
||
'post_scheduled' => 'When a post is scheduled',
|
||
],
|
||
'post_published_hint' => 'Runs whenever any post in this workspace is published. The published post becomes available at {{ trigger.post }} for downstream nodes.',
|
||
'post_scheduled_hint' => 'Runs whenever any post in this workspace is scheduled. The scheduled post is available at {{ trigger.post }}.',
|
||
|
||
'schedule' => [
|
||
'field' => 'Trigger interval',
|
||
'fields' => [
|
||
'minutes' => 'Minutes',
|
||
'hours' => 'Hours',
|
||
'days' => 'Days',
|
||
'weeks' => 'Weeks',
|
||
'months' => 'Months',
|
||
],
|
||
'minutes_interval' => 'Minutes between triggers',
|
||
'hours_interval' => 'Hours between triggers',
|
||
'days_interval' => 'Days between triggers',
|
||
'hour' => 'Trigger at hour',
|
||
'minute' => 'Trigger at minute',
|
||
'weekdays' => 'Trigger on weekdays',
|
||
'day_of_month' => 'Day of month',
|
||
'weekday_names' => [
|
||
'sun' => 'Sun',
|
||
'mon' => 'Mon',
|
||
'tue' => 'Tue',
|
||
'wed' => 'Wed',
|
||
'thu' => 'Thu',
|
||
'fri' => 'Fri',
|
||
'sat' => 'Sat',
|
||
],
|
||
'summary' => [
|
||
'every_n_minutes' => 'Runs every minute|Runs every :count minutes',
|
||
'every_n_hours' => 'Runs every hour at minute :minute|Runs every :count hours at minute :minute',
|
||
'every_n_days' => 'Runs every day at :time|Runs every :count days at :time',
|
||
'weekly' => 'Runs every :days at :time',
|
||
'monthly' => 'Runs on day :day of every month at :time',
|
||
],
|
||
],
|
||
],
|
||
'generate' => [
|
||
'social_accounts' => 'Social accounts',
|
||
'social_accounts_empty' => 'No connected social accounts. Connect one first.',
|
||
'target_slide_count' => 'Slides to generate',
|
||
'prompt_template' => 'Prompt template',
|
||
'prompt_template_hint' => 'Type {{ to insert data from earlier steps.',
|
||
'image_count' => 'Images to generate',
|
||
'image_count_hint' => '0 = text-only post (no image). 1 = single image. 2+ = carousel.',
|
||
'use_brand_voice' => 'Use brand voice',
|
||
'use_brand_voice_hint' => 'Apply your brand description and voice. Turn off for faithful curation of third-party sources (news, RSS).',
|
||
'use_brand_visuals' => 'Use brand visuals',
|
||
'use_brand_visuals_hint' => 'Steer AI images with your brand colors and identity. Turn off for neutral imagery driven only by the post topic.',
|
||
'style' => 'Style',
|
||
'account_summary' => ':count account · :format|:count accounts · :format',
|
||
'formats' => [
|
||
'single' => 'single',
|
||
'carousel' => 'carousel',
|
||
],
|
||
],
|
||
'delay' => [
|
||
'duration' => 'Duration',
|
||
'unit' => 'Unit',
|
||
'units' => [
|
||
'minutes' => 'Minutes',
|
||
'hours' => 'Hours',
|
||
'days' => 'Days',
|
||
],
|
||
],
|
||
'condition' => [
|
||
'field' => 'Field',
|
||
'operator' => 'Operator',
|
||
'operators' => [
|
||
'contains' => 'contains',
|
||
'not_contains' => 'not contains',
|
||
'equals' => 'equals',
|
||
'not_equals' => 'not equals',
|
||
'matches' => 'matches (regex)',
|
||
'greater_than' => 'greater than',
|
||
'less_than' => 'less than',
|
||
],
|
||
'value' => 'Value',
|
||
],
|
||
'publish' => [
|
||
'mode' => 'Mode',
|
||
'modes' => [
|
||
'now' => 'Publish now',
|
||
'scheduled' => 'Schedule',
|
||
'draft' => 'Save as draft',
|
||
],
|
||
'scheduled_offset' => 'Offset from trigger (minutes)',
|
||
'offset_summary' => ':mode · +:offset min',
|
||
],
|
||
'end' => [
|
||
'reason' => 'Reason (optional)',
|
||
'reason_placeholder' => 'e.g. Filtered out by condition',
|
||
],
|
||
'fetch_rss' => [
|
||
'feed_url' => 'Feed URL',
|
||
'feed_url_hint' => 'On first run, the watermark is set to "now" so historical items don\'t flood downstream nodes. Subsequent runs only see items newer than the previous poll.',
|
||
'inspect' => 'Inspect feed',
|
||
'inspecting' => 'Inspecting…',
|
||
'inspect_hint' => 'Fetch a sample to discover the available fields for use in downstream nodes.',
|
||
'inspect_error' => 'Could not read this feed. Check the URL and try again.',
|
||
'discovered_fields' => 'Available fields',
|
||
'discovered_empty' => 'No fields found in the latest item.',
|
||
],
|
||
'http_request' => [
|
||
'url' => 'URL',
|
||
'method' => 'Method',
|
||
'auth_type' => 'Authentication',
|
||
'auth' => [
|
||
'none' => 'None (public)',
|
||
'bearer' => 'Bearer token',
|
||
'basic' => 'Basic auth',
|
||
'api_key' => 'API key header',
|
||
],
|
||
'bearer_token' => 'Bearer token',
|
||
'basic_username' => 'Username',
|
||
'basic_password' => 'Password',
|
||
'api_key_header' => 'Header name',
|
||
'api_key_value' => 'API key',
|
||
'body_template' => 'Body template (JSON)',
|
||
'headers' => 'Headers',
|
||
'header_name' => 'Header name',
|
||
'header_value' => 'Value',
|
||
'add_header' => 'Add header',
|
||
'polling_section' => 'List & deduplication (optional)',
|
||
'polling_hint' => 'When the response is a list, each item runs the workflow separately. A single object runs once.',
|
||
'items_path' => 'Items path',
|
||
'items_path_hint' => 'Leave blank if the response is already an array. Use a dotted path (e.g. data.items) for a nested array, or * for an object keyed by id.',
|
||
'item_key_path' => 'Item key path',
|
||
'item_key_path_hint' => 'JSON path to a unique id (e.g. id). Items already seen are skipped, so a feed without dates still only forwards new entries.',
|
||
'item_date_path' => 'Item date path',
|
||
'item_date_path_hint' => 'JSON path to the item timestamp (e.g. published_at). Preferred over the key path when available. The first poll records the baseline and forwards nothing, so an existing feed never floods on day one.',
|
||
],
|
||
],
|
||
|
||
'delete' => [
|
||
'title' => 'Delete automation',
|
||
'description' => 'Are you sure you want to delete this automation? All runs and trigger items will also be removed. This action cannot be undone.',
|
||
'confirm' => 'Delete',
|
||
'cancel' => 'Cancel',
|
||
],
|
||
|
||
'flash' => [
|
||
'deleted' => 'Automation deleted successfully!',
|
||
],
|
||
|
||
'errors' => [
|
||
'no_active_social_accounts' => 'No active social accounts configured for this automation.',
|
||
'must_have_one_trigger' => 'Automation must have exactly one trigger node.',
|
||
'trigger_must_be_connected' => 'Trigger node must be connected to at least one node.',
|
||
'graph_contains_cycle' => 'Automation graph contains a cycle.',
|
||
'only_failed_can_retry' => 'Only failed runs can be retried.',
|
||
'no_generated_post' => 'No generated post found on run.',
|
||
'url_not_allowed' => 'The request URL points to a private or unreachable address and was blocked.',
|
||
'node_no_longer_exists' => 'Node :node_id no longer exists in the automation.',
|
||
'no_trigger_connection' => 'No node connected to the Trigger node.',
|
||
'fetch_rss_missing_url' => 'The Fetch RSS node is missing a feed URL.',
|
||
'fetch_rss_request_failed' => 'The RSS feed request failed.',
|
||
'fetch_rss_malformed' => 'The RSS feed is malformed.',
|
||
'http_missing_url' => 'The HTTP request node is missing a URL.',
|
||
'http_request_exception' => 'The HTTP request threw an exception.',
|
||
'http_request_failed' => 'The HTTP request failed.',
|
||
'http_items_path_not_array' => 'The items path did not resolve to a list.',
|
||
'generate_image_format_required' => 'AI generate only creates images. Pick an image format (not video).',
|
||
],
|
||
];
|