trypost/app/Actions/Automation/Node/RunWebhookNode.php
Paulo Castellano 4efaa0bf99 Harden automations module: full-post generation, reliable runs, editor UX
Generation
- Generate node now produces the full post (text + AI image + carousel)
  via a shared PostImagePipeline extracted from StreamPostCreation
- Generate config UI mirrors the /posts/create wizard (carousel slide
  count, include-image toggle); drop the decorative format/unsplash keys

Flow correctness
- RSS/HTTP nodes expose named has-items (default) and no-items output
  handles, labeled and colored like the Condition node
- AdvanceAutomationRun records a no_matching_edge terminal instead of
  completing silently; "0 new items" feedback in the test panel
- Manual/test runs no longer persist the production dedup watermark

Run reliability
- Pause truly halts in-flight runs (production only; manual test runs
  always run regardless of automation status)
- ProcessAutomationNode::failed() marks the run failed
- automation:recover-stuck-runs and automation:prune-dry-runs commands

Webhook / HTTP
- Branded User-Agent (config-driven) on outbound webhook + http_request
- Webhook fails on invalid JSON instead of silently sending {}
- HTTP custom headers editor; CodeMirror-based CodeEditor for JSON

Editor UX
- Header Test button only opens the panel; the panel has a Run button
  (saves first) and owns the with-real-data toggle
- Clicking a node closes the test panel and opens its config
- Node cards: max-width + truncate so long URLs don't grow the node
2026-06-10 20:45:01 -03:00

61 lines
2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Actions\Automation\Node;
use App\DataTransferObjects\Automation\NodeRunResult;
use App\Models\AutomationRun;
use App\Services\Automation\ExpressionResolver;
use Illuminate\Support\Facades\Http;
class RunWebhookNode
{
public function __construct(private ExpressionResolver $resolver) {}
public function __invoke(AutomationRun $run, array $config): NodeRunResult
{
$url = $this->resolver->resolve($config['url'] ?? '', $run->context ?? []);
$method = strtoupper($config['method'] ?? 'POST');
$headers = [];
foreach ($config['headers'] ?? [] as $k => $v) {
$headers[$k] = $this->resolver->resolve((string) $v, $run->context ?? []);
}
$payloadJson = $this->resolver->resolve($config['payload_template'] ?? '{}', $run->context ?? []);
$trimmedPayload = trim($payloadJson);
if ($trimmedPayload !== '' && $trimmedPayload !== 'null') {
$decoded = json_decode($payloadJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return NodeRunResult::failed(__('automations.errors.webhook_invalid_payload_json'), [
'reason' => 'invalid_payload_json',
]);
}
$payload = $decoded ?? [];
} else {
$payload = [];
}
$response = Http::withHeaders($headers)
->withUserAgent(config('trypost.user_agent'))
->send($method, $url, ['json' => $payload]);
if ($response->serverError()) {
return NodeRunResult::failed(__('automations.errors.webhook_server_error'), [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
]);
}
return NodeRunResult::completed(output: [
'webhook' => [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
],
]);
}
}