2026-05-24 12:17:19 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\App;
|
|
|
|
|
|
|
|
|
|
use App\Actions\Automation\Automation\ActivateAutomation;
|
|
|
|
|
use App\Actions\Automation\Automation\CreateAutomation;
|
2026-06-10 21:10:13 +00:00
|
|
|
use App\Actions\Automation\Automation\DeleteAutomation;
|
|
|
|
|
use App\Actions\Automation\Automation\GetAutomationEditorData;
|
2026-06-12 22:19:46 +00:00
|
|
|
use App\Actions\Automation\Automation\GetAutomationInvocations;
|
|
|
|
|
use App\Actions\Automation\Automation\GetAutomationMetrics;
|
2026-06-10 21:10:13 +00:00
|
|
|
use App\Actions\Automation\Automation\ListAutomations;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Actions\Automation\Automation\PauseAutomation;
|
|
|
|
|
use App\Actions\Automation\Automation\UpdateAutomation;
|
|
|
|
|
use App\Actions\Automation\Run\RetryRunFromNode;
|
|
|
|
|
use App\Actions\Automation\Run\TestAutomation;
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use App\Http\Requests\App\Automations\ActivateAutomationRequest;
|
|
|
|
|
use App\Http\Requests\App\Automations\PauseAutomationRequest;
|
|
|
|
|
use App\Http\Requests\App\Automations\RetryRunRequest;
|
|
|
|
|
use App\Http\Requests\App\Automations\StoreAutomationRequest;
|
|
|
|
|
use App\Http\Requests\App\Automations\TestAutomationRequest;
|
|
|
|
|
use App\Http\Requests\App\Automations\UpdateAutomationRequest;
|
|
|
|
|
use App\Http\Resources\App\PlatformConfigResource;
|
|
|
|
|
use App\Http\Resources\App\SocialAccountResource;
|
2026-06-12 22:19:46 +00:00
|
|
|
use App\Http\Resources\AutomationInvocationResource;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Http\Resources\AutomationNodeRunResource;
|
|
|
|
|
use App\Http\Resources\AutomationResource;
|
|
|
|
|
use App\Http\Resources\AutomationRunResource;
|
|
|
|
|
use App\Models\Automation;
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
use App\Models\AutomationNodeRun;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Models\AutomationRun;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\RedirectResponse;
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Response;
|
|
|
|
|
|
|
|
|
|
class AutomationController extends Controller
|
|
|
|
|
{
|
2026-06-10 21:10:13 +00:00
|
|
|
public function index(ListAutomations $list): Response
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
2026-06-10 21:10:13 +00:00
|
|
|
$workspace = request()->user()->currentWorkspace;
|
|
|
|
|
|
2026-05-24 12:17:19 +00:00
|
|
|
$automations = Inertia::scroll(fn () => AutomationResource::collection(
|
2026-06-10 21:10:13 +00:00
|
|
|
$list($workspace)
|
2026-05-24 12:17:19 +00:00
|
|
|
));
|
|
|
|
|
|
|
|
|
|
return Inertia::render('automations/Index', [
|
|
|
|
|
'automations' => $automations,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function store(StoreAutomationRequest $request, CreateAutomation $create): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$automation = $create(
|
|
|
|
|
$request->user()->currentWorkspace,
|
|
|
|
|
$request->user(),
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-12 22:19:46 +00:00
|
|
|
return redirect()->route('app.automations.workflow', $automation->id);
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 22:19:46 +00:00
|
|
|
public function show(Automation $automation): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
return redirect()->route('app.automations.workflow', $automation->id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function workflow(Automation $automation, GetAutomationEditorData $editorData): Response
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
|
|
|
|
$this->authorize('update', $automation);
|
|
|
|
|
|
2026-06-10 21:10:13 +00:00
|
|
|
['socialAccounts' => $socialAccounts, 'pinterestBoards' => $pinterestBoards, 'tiktokCreatorInfos' => $tiktokCreatorInfos] = $editorData($automation);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
$platformConfigs = $socialAccounts->mapWithKeys(fn ($account) => [
|
|
|
|
|
$account->id => new PlatformConfigResource($account),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return Inertia::render('automations/Form', [
|
|
|
|
|
'automation' => AutomationResource::make($automation),
|
|
|
|
|
'socialAccounts' => SocialAccountResource::collection($socialAccounts),
|
|
|
|
|
'platformConfigs' => $platformConfigs,
|
|
|
|
|
'pinterestBoards' => $pinterestBoards,
|
|
|
|
|
'tiktokCreatorInfos' => $tiktokCreatorInfos,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 22:19:46 +00:00
|
|
|
public function invocations(Automation $automation, GetAutomationInvocations $invocations): Response
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('view', $automation);
|
|
|
|
|
|
|
|
|
|
$status = request()->string('status')->toString() ?: null;
|
|
|
|
|
$search = request()->string('search')->toString() ?: null;
|
|
|
|
|
|
|
|
|
|
return Inertia::render('automations/Invocations', [
|
|
|
|
|
'automation' => AutomationResource::make($automation),
|
|
|
|
|
'invocations' => Inertia::scroll(fn () => AutomationInvocationResource::collection(
|
|
|
|
|
$invocations($automation, $status, $search)
|
|
|
|
|
)),
|
|
|
|
|
'filters' => [
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'search' => $search,
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function settings(Automation $automation): Response
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('view', $automation);
|
|
|
|
|
|
|
|
|
|
return Inertia::render('automations/Settings', [
|
|
|
|
|
'automation' => AutomationResource::make($automation),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function metrics(Automation $automation, GetAutomationMetrics $metrics): Response
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
|
|
|
|
$this->authorize('view', $automation);
|
|
|
|
|
|
2026-06-12 22:19:46 +00:00
|
|
|
$end = (request()->date('end') ?? now())->startOfDay();
|
|
|
|
|
$start = (request()->date('start') ?? now()->subDays(6))->startOfDay();
|
|
|
|
|
|
|
|
|
|
if ($start->greaterThan($end)) {
|
|
|
|
|
[$start, $end] = [$end, $start];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cap the window so a hand-edited URL can't request a multi-year, daily
|
|
|
|
|
// bucketed series.
|
|
|
|
|
if ($start->diffInDays($end) > 366) {
|
|
|
|
|
$start = $end->copy()->subDays(366);
|
|
|
|
|
}
|
2026-06-10 21:10:13 +00:00
|
|
|
|
2026-06-12 22:19:46 +00:00
|
|
|
return Inertia::render('automations/Metrics', [
|
2026-05-24 12:17:19 +00:00
|
|
|
'automation' => AutomationResource::make($automation),
|
2026-06-12 22:19:46 +00:00
|
|
|
'metrics' => $metrics($automation, $start, $end),
|
|
|
|
|
'filters' => [
|
|
|
|
|
'start' => $start->toDateString(),
|
|
|
|
|
'end' => $end->toDateString(),
|
|
|
|
|
],
|
2026-05-24 12:17:19 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function update(UpdateAutomationRequest $request, Automation $automation, UpdateAutomation $update): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('update', $automation);
|
|
|
|
|
|
|
|
|
|
$update($automation, $request->validated());
|
|
|
|
|
|
|
|
|
|
return back();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 21:10:13 +00:00
|
|
|
public function destroy(Automation $automation, DeleteAutomation $delete): RedirectResponse
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
|
|
|
|
$this->authorize('delete', $automation);
|
2026-06-10 21:10:13 +00:00
|
|
|
$delete($automation);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
session()->flash('flash.banner', __('automations.flash.deleted'));
|
|
|
|
|
session()->flash('flash.bannerStyle', 'success');
|
|
|
|
|
|
|
|
|
|
return redirect()->route('app.automations.index');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function activate(ActivateAutomationRequest $request, Automation $automation, ActivateAutomation $activate): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('activate', $automation);
|
|
|
|
|
|
|
|
|
|
$activate($automation);
|
|
|
|
|
|
|
|
|
|
return back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pause(PauseAutomationRequest $request, Automation $automation, PauseAutomation $pause): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('pause', $automation);
|
|
|
|
|
|
|
|
|
|
$pause($automation);
|
|
|
|
|
|
|
|
|
|
return back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function retryRun(
|
|
|
|
|
RetryRunRequest $request,
|
|
|
|
|
RetryRunFromNode $retry,
|
|
|
|
|
Automation $automation,
|
|
|
|
|
AutomationRun $run,
|
|
|
|
|
): \Illuminate\Http\Response {
|
|
|
|
|
$this->authorize('update', $automation);
|
|
|
|
|
abort_unless($run->automation_id === $automation->id, 404);
|
|
|
|
|
|
|
|
|
|
$nodeId = $request->validated('node_id') ?? $run->current_node_id;
|
|
|
|
|
$retry($run, $nodeId);
|
|
|
|
|
|
|
|
|
|
return response()->noContent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test(TestAutomationRequest $request, Automation $automation, TestAutomation $test): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('update', $automation);
|
|
|
|
|
|
|
|
|
|
$run = $test($automation, (bool) $request->validated('with_real_data', false));
|
|
|
|
|
|
|
|
|
|
return response()->json(['run_id' => $run->id]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function showRun(Automation $automation, AutomationRun $run): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$this->authorize('view', $automation);
|
|
|
|
|
abort_unless($run->automation_id === $automation->id, 404);
|
|
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
// Aggregate the node runs of every branch forked by a fan-out so the test
|
|
|
|
|
// panel shows the whole execution, not just the branch the root walked.
|
|
|
|
|
$rootId = $run->rootId();
|
|
|
|
|
|
|
|
|
|
$nodeRuns = AutomationNodeRun::query()
|
|
|
|
|
->whereHas('run', fn ($query) => $query
|
|
|
|
|
->where('id', $rootId)
|
|
|
|
|
->orWhere('root_run_id', $rootId))
|
|
|
|
|
->orderBy('started_at')
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->get();
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'run' => AutomationRunResource::make($run)->resolve(),
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
'node_runs' => AutomationNodeRunResource::collection($nodeRuns)->resolve(),
|
2026-05-24 12:17:19 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|