GenerateNodeValidator and the Generate UI now respect ContentType::minMediaCount, and content-type listings share accept/min flags via toListingArray().
Co-authored-by: Cursor <cursoragent@cursor.com>
AI generate only produces images, so Video Pin / Reel / TikTok Video are filtered out in previewOnly and rejected server-side with a clear error.
Co-authored-by: Cursor <cursoragent@cursor.com>
Empty-account mount was clamping target_slide_count to 0 and never raising it when a media-required account was selected, so Pinterest showed a false requires-media error after picking a board.
Co-authored-by: Cursor <cursoragent@cursor.com>
Share {boards, truncated} via ListPinterestBoards into Inertia and warn in the board picker when pagination stopped early, matching API/MCP.
Co-authored-by: Cursor <cursoragent@cursor.com>
getBoards now returns {boards, truncated}; pass only the boards array into Inertia pinterestBoards so post and automation pickers keep receiving an array.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject CreatePost calls without CreatedVia, set Web on DuplicatePost, and assert wiring for templates, AI, automation, and API spoof attempts.
Co-authored-by: Cursor <cursoragent@cursor.com>
- automations: minimal back-only header on mobile for the workflow builder
and detail tabs (extracted AutomationMobileBackHeader); open live
automations on the metrics tab (drafts still open on workflow); full-width
status filter + refresh on the invocations toolbar; use IconMenu2 for the
mobile sidebar trigger
- dialogs: stack DialogFooter primary-on-top / cancel-at-bottom on mobile
- workspaces: bring the workspace picker cards into the neo-brutalist design
- posts: left-align the label filter content; wrap the post-view date/status
header so a long status badge no longer squeezes the date; add hamburger
clearance to the editor's mobile tab bar
RunFetchRssNode, RunWebhookNode and RunHttpRequestNode guarded the initial URL but then followed redirects unguarded, so a public URL could 302 to an internal address. RSS now fetches through SafeHttpFetcher::get() (re-guards every hop); webhooks no longer follow redirects; the generic HTTP request node re-runs the SSRF guard on each hop via a new SafeHttpFetcher::redirectGuardOptions().
Collapse LinkedIn to one content type per account kind (linkedin_post, linkedin_page_post). Publishers infer the publish format from the attached media — text, single image/video, multi-image carousel, or PDF document — matching how facebook_post/x_post already work; PDF is exclusive of any other attachment. Removes the editor variant picker, keeping only the PDF document title field. Includes a data migration collapsing the retired carousel/document content types.
Replace the two LinkedIn account cards with a single Connect LinkedIn button: one unified OAuth grant (linkedin-openid driver, union of scopes) then a post-callback identity picker to post as the personal profile (linkedin) or a company page the member administers (linkedin-page). The chosen organization is validated against the admin-verified list from the OAuth grant. Per-capability gating via LINKEDIN_ENABLED / LINKEDIN_PAGE_ENABLED supports profile-only or org-only self-hosting. Removes LinkedInPageController, LinkedInTokenSynchronizer, the standalone linkedin-page connect routes, and the unused redirect_page config.
Viewers could mutate posts, automations and trigger AI write endpoints,
and every role saw create/manage affordances that 403'd on click.
Backend (security):
- PostPolicy update/delete now require member+ (was tenancy-only), which
also gates the AI write endpoints that authorize('update')
- AutomationPolicy create/update/delete require member+; activate/pause
delegate to update
- AutomationController authorizes index/store/show; AnalyticsController
authorizes view
- Comments stay open to members incl. viewer (by design)
Frontend (UI gating via new useWorkspaceRole composable):
- Sidebar: create post / create workspace / automations / library nav
- Accounts grid: connect / disconnect / reconnect (admin+)
- Members: invite / change role / remove / cancel invite (admin+)
- Account billing tab (owner); posts index + calendar create affordances
Tests: PostPolicyTest, AutomationPolicyTest (all four roles) and an
end-to-end WorkspaceRolePermissionsTest; aligned the automation test
suites' account/workspace setup with role pivots.
The Generate node data rules had no rule for 'style', so validated()
dropped it on save — the run always fell back to the default image_card.
Add a Rule::in(ContentStyle) rule so the selected style is persisted and
unknown values are rejected.
Replace the 'single'/'carousel' string format threaded through the AI
generation path with a typed GeneratorFormat enum (PostContentGenerator,
PostContentHumanizer, PostContentStreamer, StreamPostCreation,
RunGenerateNode, deriveFormat).
Remove generatorFormat() from AiContentTemplate and the three templates —
it conflated style and format (returning 'tweet_card'). The tweet-card
check now lives on ContentStyle via isTweetCard(), and the humanizer skip
on ContentStyle::humanizes(), so the style declares its own behaviour
instead of callers comparing magic strings.
- inspectFeed fetches via SafeHttpFetcher::get (SSRF guard + timeout + redirect
cap + UA) instead of a raw, timeout-less Http::get; add a timeout to the node fetch.
- ResolvableUrl now requires an http(s) scheme, so it's no weaker than the plain
url rule it replaced (rejects file://, javascript://).
- Apply ResolvableUrl to the HTTP Request and Webhook node URLs too, so templated
{{ }} URLs validate consistently across every URL field.
- Clear a fetch_rss node's discovered_fields when its feed_url changes, so stale
fields from the previous feed stop showing up as autocomplete suggestions.
A manual/dry "test with real data" run surfaces a single item and never fans
out, but it was taking the oldest new item (items are sorted oldest-first for
production fan-out). Surface the newest item in preview instead — what the user
expects to test against. Real runs are unchanged. Works for every feed format.
Move the RSS sample feeds out of inline heredocs/constants in FetchRssNodeTest
into tests/fixtures/feeds/, matching the parser fixtures. Centralize the
feedFixture() loader in tests/Pest.php and use it across the feed tests.
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
- Add AutomationRun::durationInMilliseconds() as the single source of truth
for the Invocations list and metrics, replacing the duplicated inline diff.
- Fold the variables and root_run_id columns into their create migrations
(this branch isn't in production) and drop the standalone alters.
- Import Illuminate\Http\Response (aliased) instead of referencing it inline.
A webhook node parses its payload template as JSON before resolving
placeholders, so a template with unquoted {{ }} placeholders or any malformed
JSON could be saved, tested, and activated — only to fail midway through a run.
Reject it up front instead: AutomationConfigValidator is the single source of
truth for per-node config issues (keyed to the field the editor surfaces them
under), enforced on save (field errors), on activate, and before a test run.
The editor mirrors the check to disable Test/Activate with a clear reason, and
the test panel now surfaces the server's message instead of a generic toast.
The scheduler command ran every minute and loaded all active automations,
then filtered by trigger_type in PHP because that value lived buried in the
nodes JSON array — effectively a full-table scan plus a JSON decode per row
each minute, discarding every non-schedule automation.
Derive trigger_type into a real, indexed column on save (recomputed in the
existing saving() hook so it can never drift from nodes) and filter on it in
SQL. Applies to both the schedule firer and the post-trigger dispatcher.
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
- Pagination: drop the perPage override from ListAutomations and the
hardcoded page size from GetAutomationInvocations; both use
config('app.pagination.default'). Document the rule in CLAUDE.md.
- Imports: import DomainException / InvalidArgumentException / Throwable
instead of inline backslash references across the automation actions.
- i18n: move the hardcoded Fetch RSS and HTTP Request failure strings to
automations.errors.* in all three locales.
- Publish: require scheduled_offset when mode is scheduled (validation)
and drop the magic 60-minute default in the node. Fixes the required_if
rules to reference the concrete node index instead of a wildcard that
never resolved (also repairs the trigger cron rule).
- Condition handles: back the yes/no output handles with a shared
Condition\Handle enum (PHP) and ConditionHandle const (TS).
Remove the "Custom (Cron)" schedule field — too technical for the editor.
The remaining presets (minutes/hours/days/weeks/months) cover the need and
still build the cron string under the hood.
Removed at the root: the ScheduleField.Custom enum case, the custom-cron
input and select option, the schedule_custom_cron type field and its
schedule-summary handling, the backend validation (Rule::in and the
schedule_custom_cron rule), the i18n keys, and the custom round-trip test
case. Existing automations keep firing — the scheduler runs off the stored
cron string, not schedule_field.
Wrap the webhook HTTP send in a try/catch so a connection error returns
a clean failed result (reason: request_failed) instead of bubbling up as
a job failure — matching the HTTP request node.
Add tests for the gaps in node-run coverage:
- HTTP request: basic auth, PUT/PATCH/DELETE, non-2xx responses,
connection exceptions, and an items_path that doesn't resolve to a list.
- Webhook: every HTTP method, header expression resolution, and the new
connection-failure path.
- Fetch RSS: non-2xx feed responses, malformed XML, items without a
publish date (skipped), and the link fallback when an item has no guid.
- Delay: unknown unit throws.
- Publish: dry runs don't publish or queue.
Split the automation detail screen into four route-based tabs behind a
shared AutomationHeader:
- Workflow: the existing editor canvas.
- Invocations: a paginated, filterable run log with expandable per-node
detail, a refresh control, and a loading state.
- Metrics: KPI cards, a runs-over-time @unovis chart with locale-aware
date labels, and a posts-by-platform breakdown over a date range.
- Settings: rename, an activate/pause switch, and a danger-zone delete.
Invocations and Metrics report only real executions via a new
productionRuns scope, so manual test runs (dry or with real data) never
leak into the log or the charts. The now-unused excludingDryRuns scope
is removed.
Generated copy now flows the most-restrictive platform context through
the humanizer too, and the editor guide documents every available
expression grouped by source node.
Replace free-text brand_tone/brand_voice_notes with a single structured
brand_voice_traits JSON column backed by the BrandVoiceTrait enum, exposed
as choice-chip pills in the brand settings UI and autofillable from a site.
Brand voice and visuals become per-automation toggles on the Generate node.
Unify the image controls into one 0-10 picker (0 = text-only, 1 = single,
2+ = carousel) and feed the generator the most restrictive selected network
so copy fits every platform. Pass that same platform context through the
humanizer pass — extracted into a shared ResolvesPlatformCopyBudget trait —
so the rewrite can no longer drift past the character cap the generator
respected, in both the automation and manual creation flows.
Persist the trigger node's schedule editor fields on save (they were
silently dropped by validated() for lacking validation rules).
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).
- Added support for workflow variables in automations, allowing users to define reusable values.
- Implemented validation for Generate nodes to ensure intended image counts align with selected accounts.
- Updated automation models and requests to handle new variables, including encryption for sensitive data.
- Enhanced UI to display variables and their management within the automation editor.
- Improved error handling for webhook and HTTP nodes to prevent requests to invalid URLs.
- Refactored various components for better context resolution during automation runs.
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
Conflict resolutions + integration fixes:
- CreatePost: kept the branch's merge-into-existing meta persistence (equivalent
to main's #86 replace on create, and what the automations flow was built on).
- FacebookSettings.vue: kept both new defaults (previewOnly + meta).
- RunGenerateNode + GenerateNodeConfig.vue: ContentType::InstagramCarousel was
removed on main (#80); an IG carousel is now a multi-image instagram_feed, so
the carousel-capable list uses InstagramFeed.
- GenerateNodeTest: fixtures use the ContentType enum and the new instagram_feed
carousel signal.
- Added new automation-related routes and controllers for managing automations.
- Introduced automation nodes in the UI with distinct styles and interactions.
- Updated sidebar to include navigation for automations.
- Enhanced post creation logic to support automation metadata.
- Refactored content type and platform enums into types for better type safety.
- Added localization for automation-related terms in English, Spanish, and Portuguese.
- Improved error handling in various components to accommodate new features.