Commit graph

7 commits

Author SHA1 Message Date
Paulo Castellano
d8149ef058
Remove the automations module (#333)
* Remove the automations module

Drops the visual workflow builder end to end: actions, node runners,
commands, jobs, models, observers, policy, resources, requests, routes,
broadcast channel, scheduler entries, Horizon supervisor, Vue pages and
components, canvas undo/redo history, CodeEditor, translations, factories
and tests.

A new migration rewrites posts.created_via = 'automation' to 'web' and
drops the five automation tables. The CreatedVia::Automation enum case is
removed. The 2026-08-21 social-account identity migration now skips its
automation node repointing when the automations table no longer exists,
so it stays re-runnable after the drop.

Orphaned dependencies removed: simplepie/simplepie, @vue-flow/*,
codemirror and @codemirror/*.

* Drop the orphaned common.beta translation key

* Remove automation leftovers: ResolvableUrl rule, feed fixtures, useShortcut, nav badge

* Drop the unused chart wrapper and @unovis packages

* Address review: keep the shipped migration untouched, drop the dead previewOnly chain

- Restore 2026_08_21 migration to exactly what production ran; the
  rehearsal test now recreates the automations table it expects instead.
- Mark the drop migration's down() irreversible like its siblings.
- Simplify DropAutomationTablesMigrationTest to the sibling shape.
- Remove previewOnly / aiGenerateVariants: the only caller that set the
  prop was the deleted automation Generate node.
- Run pint over lang/*/common.php after the beta key removal.

* Exercise the drop migration against the real automation tables and their FKs

* Drop DuplicateIdentityRehearsalTest: it re-ran a frozen migration that reads the removed automations table
2026-09-05 11:03:23 -03:00
Paulo Castellano
58d8e066b5
Add workspace webhooks and drop the unused automation webhook node (#326)
* 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.
2026-09-04 09:43:29 -03:00
Paulo Castellano
a1a9172448
fix: subscribe to AI generation channel before dispatching the job (#269)
* fix: subscribe to AI generation channel before dispatching the job

AiGenerateDialog posted the generate request and only subscribed to the
broadcast channel once the response came back. Reverb's plain Channel
(vendor/laravel/reverb/.../Channels/Channel.php) delivers broadcasts by
iterating the in-memory connection list at send time only — no history,
no replay. If the queued job started streaming text_delta/stream_end
events before the private-channel subscribe handshake finished, those
events were gone for good and the dialog hung on 'streaming' forever.

Generate the generation_id client-side, await channel subscription
confirmation (Echo's channel.subscribed(), backed by
pusher:subscription_succeeded) before sending the generate request, and
have the backend use the client-supplied id instead of minting its own.

Related to #218.

* fix: handle subscribe failure, dialog-close race, and i18n hardcoded strings

Code review on the previous commit surfaced real gaps:

- useAiStream only handled the subscribe-success path (.subscribed()).
  A definitive pusher:subscription_error (expired session, CSRF
  mismatch) fell through to the 5s timeout and was treated as success,
  reproducing the exact #218 hang via a different trigger. subscribe()
  now returns a boolean: true on confirm-or-ambiguous-timeout (proceed
  optimistically), false only on an explicit error (don't dispatch work
  nothing will ever hear).
- AiGenerateDialog now bails without dispatching if the dialog is
  closed while awaiting subscription confirmation (up to 5s), and
  unsubscribes on a post-subscribe dispatch failure so stale listeners
  can't flip status to 'completed' after an error was already shown.
- Extracted aiGenerationChannel() so the frontend has one place to
  update if the channel format changes, and cross-referenced the two
  remaining backend copies (PostAiGenerateController, StreamPostContent).
- Replaced the two hardcoded English fallback error strings with i18n
  keys (posts.ai.generate.errors.*) across all 16 locales — this app
  only ships translated copy.
- Simplified subscribe(): dropped a redundant "already settled" guard
  and the clearTimeout bookkeeping — Promise.resolve() already no-ops
  after the first call, so there was nothing to guard.

* fix: subscribe before dispatch in AI post creation and image regeneration flows

Extracts subscribePrivateChannel as a shared helper and applies the same
subscribe-before-dispatch fix from the post edit flow (issue #218) to the
other two flows that stream over private channels.

Post creation wizard moves the StreamPostCreation dispatch out of
AiPostWizard.vue and into Loading.vue, so the channel is subscribed before
the job that broadcasts on it is dispatched — previously a full page
navigation happened between dispatch and subscribe with no timeout
fallback, so a lost event hung the page forever.

* refactor: trim explanatory comments added in the subscribe-before-dispatch fix

Rationale lives in the commit history, not scattered inline comments.

* refactor: trim remaining explanatory comments from the subscribe-before-dispatch fix

Same cleanup as the previous commit, applied to the files from the
original post-edit-flow fix.

* fix: dedup post-creation dispatch, surface field-level AI errors, harden edge cases

Uses ShouldBeUnique on StreamPostCreation (scoped per-user) instead of an
ad-hoc cache guard, matching the pattern already used by
PublishToSocialPlatform and VerifyUpcomingPostConnections.

Field-level validation errors (prompt/instruction) now render inline via
InputError, matching how every other form in the app surfaces them, instead
of a generic status banner. Also fixes a 422 response missing the `errors`
key that Inertia's client silently swallows, adds error handling around the
wizard's navigation to the loading page, and extracts the duplicated
error-message parsing into a shared helper.
2026-08-10 16:06:37 -03:00
Paulo Castellano
b23ab0166e feat(automations): implement automation features and UI enhancements
- 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.
2026-05-24 09:17:19 -03:00
Paulo Castellano
efdf902b80 fix(ai): wire up streaming generate dialog (event names + JSON extract + loading state)
The inline "Generate post with AI" dialog never showed any output, even
after the queue job finished. Three independent issues stacked:

1. Event name mismatch in useAiStream. Listeners were CamelCased
   (.TextDelta, .StreamEnd, .Error) but laravel/ai broadcasts events
   with snake_case names via StreamEvent::type() (text_delta, stream_end,
   error). Echo never matched, the listener never fired, status stuck on
   'streaming' forever. Renamed to snake_case.

2. The streamer agent shares the structured-output prompt template with
   PostContentGenerator, so the stream emits a JSON object like
   {"content":"...","image_title":"...","image_keywords":[...]}, not
   plain caption text. Added a previewText computed in AiGenerateDialog
   that JSON.parse's the accumulated text and grabs .content. Mid-stream
   the parse fails, so the preview stays empty (showing the existing
   '...' placeholder) until stream_end produces a complete JSON.

3. The Generate button used v-if="status === 'idle'" — it disappeared
   the instant generation started, leaving the user with no feedback.
   Swapped to v-if="status === 'idle' || status === 'streaming'" and
   used the Button component's built-in :loading prop so it stays
   visible with a spinner inside while the agent runs.
2026-05-11 21:42:50 -03:00
Paulo Castellano
554b86d223 feat: useWorkspaceEcho / usePostEcho composables and realtime list refresh
Two new composables under resources/js/composables/echo/ that abstract
channel naming for the two Reverb subscription patterns we now use:

- useWorkspaceEcho — list/dashboard views: 1 subscription per workspace
  delivers events for many entities. Resolves currentWorkspace.id from
  Inertia shared props; no-ops when there is no workspace yet.
- usePostEcho — focused views (Show, Edit): per-post channel.

Posts index now subscribes via useWorkspaceEcho to the 3 lifecycle
events (post.created / post.deleted / post.platform.status.updated)
and reloads the posts prop on receipt. `reset: ['posts']` is required
on the reload — without it, InfiniteScroll's merge prop appends every
broadcast onto the existing list and produces duplicates.

Show / Edit migrated from raw useEcho to usePostEcho; AiGenerateDialog,
NotificationBell and AiPostWizard updated to the new event names from
the namespaced convention. AiStream composable moved into the same
echo/ folder for organisation; its bound event names (TextDelta /
StreamEnd / Error) come from the laravel/ai package and stay as-is.

Bonus: comments tab send button was overriding shadcn's size="icon"
(40px) with class="size-9" (36px), which left it visibly shorter than
the textarea. Removed the override and matched the textarea's
min-h to size-10 so both render at 40px.
2026-05-07 16:02:54 -03:00
Paulo Castellano
06e72831f6 feat: workspace-scoped broadcast channel and post lifecycle events
Adds a private workspace channel (WorkspaceChannel, authorised by
membership) so list views can subscribe once and receive events for
every post in the workspace, instead of opening N per-post channels.

New events:
- PostCreated   (broadcast on workspace.{id})
- PostDeleted   (broadcast on workspace.{id}; carries primitive ids so
  it fires after \$post->delete())
- PostPlatformStatusUpdated now broadcasts on both post.{id} and
  workspace.{id} so focused views and lists share the same trigger.

All broadcast events migrated to the namespaced 'entity.action'
convention from Laravel's broadcasting docs and switched from
ShouldBroadcastNow to ShouldBroadcast on a dedicated 'broadcasts'
queue (added to the supervisor-1 list in config/horizon.php) to keep
HTTP responses fast:

  PostCreated                  -> post.created
  PostDeleted                  -> post.deleted
  PostPlatformStatusUpdated    -> post.platform.status.updated
  PostCommentCreated           -> post.comment.created
  NotificationCreated          -> notification.created
  PostCreationReady            -> ai.creation.completed

Drops the SubscriptionCreated event — it was broadcast on
users.{owner_id} but had no listeners (frontend or backend) and the
billing/PostHog flow already handles plan-change tracking elsewhere.

PostPlatformStatusUpdated payload trimmed to {post_id} since every
consumer (Show, Edit, Index) does router.reload({ only: [...] }) and
ignored the rich shape.
2026-05-07 16:02:22 -03:00