* 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
When a social account's token expired or the connection was lost, the
Connections card only offered "Reconnect". Users who wanted to drop
that account and connect a different one had no way to do it from the UI.
The lost-connection card now renders both Reconnect and Disconnect,
reusing the existing disconnect confirmation flow. The connected-state
Disconnect button gets a data-testid too so browser tests can target
either state by account id.
* chore(release): add v1.0.9 changelog, customer email, and thumbnail artifacts
* chore(release): rewrite v1.0.9 customer email as a webhook announcement
* chore(release): tighten v1.0.9 email around what people asked for
* chore(release): rewrite v1.0.9 email around webhook value
* chore(release): write a plain v1.0.9 webhook announcement email
* chore(release): close v1.0.9 email with a G2 review ask
* Expose workspace webhooks through the API and MCP.
The same create/update/test/rotate/replay/delete flow now lives in Actions so the web UI, REST API, and MCP tools stay in lockstep.
* Keep webhook validation local to each web, API, and MCP entry point.
* Extract MCP webhook rules into request classes and close remaining API/MCP review gaps.
* Tighten webhook updates to a field whitelist and reset failures only on re-enable.
* Treat a mismatched webhook replay as not found and mark secret rotation destructive.
* Stop broadcasting webhook payloads so Reverb does not reject live log updates.
The Echo event only needs list metadata; Inertia reloads the full log after the status lands.
* Type webhook Echo payloads separately and cover delivered, failed, and pending broadcasts.
The show page still receives payload and body over HTTP; Echo only carries list metadata.
* 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.
* perf(horizon): skip queues for platforms that are disabled
The social-publishing supervisor listens on Platform::allQueues(), which maps
over every enum case regardless of the per-platform *_ENABLED toggles. With
minProcesses => 1 that means one worker per supported platform - even on an
installation that only ever connects two or three of them.
On a single-workspace self-host that was 19 Horizon workers at roughly 75 MB
each; filtering by the toggles brought it to 9 and cut container memory from
1.66 GB to 1.06 GB, with no change in publishing behaviour.
Note on the implementation: the filter reads env() directly rather than calling
Platform::isEnabled(). Config files load alphabetically, so config('trypost.*')
does not exist yet while horizon.php is evaluated - isEnabled() would silently
return its default of true and the filter would be a no-op. This bites at
config:cache time, so it is invisible in tinker.
* refactor(horizon): filter disabled platform queues via enum
Move queue filtering to Platform::enabledQueues() and apply it from
AppServiceProvider after config has loaded, avoiding env() parsing in
horizon.php while keeping isEnabled() as the single source of truth.
* refactor(horizon): use enabledQueues directly in horizon config
Remove AppServiceProvider boot override and let isEnabled() fall back
to env when trypost config is not loaded yet.
* chore: enable Eloquent strict mode in all environments
* refactor(platform): replace enabled env key derivation with explicit match
* refactor(platform): simplify isEnabled using config default fallback
* test(platform): cover publishing queues and enabled toggles exhaustively
* refactor(platform): collapse isEnabled env fallback into one method
* refactor(platform): drop filter_var and rely on env boolean casting
* revert: keep Eloquent strict mode out of production
shouldBeStrict() in production would throw on lazy loads in queued
publish jobs and can stop posting. Restore the Laravel default.
---------
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
* Fall back to display name when a published-post email has no username.
Facebook Pages often have no vanity username, which left the email as "Facebook Page (@)". Use username, then display name, and omit the empty parentheses when both are missing.
* Move the notification account label onto PostPlatform.
The fallback belongs on the row that always has a platform, so mail and in-app notify can call one method instead of guarding a missing social account at every call site.
* Cover the in-app published/failed notification body for Facebook Pages.
The email tests were not enough — SendNotification body is what the in-app inbox shows, and that path also goes through notificationLabel().
* feat: visible Terms/Privacy links on the auth screens
Social-platform app reviews (TikTok explicitly) require both links to be
reachable from the public site without logging in or opening a menu.
* Cover the legal links with tests and tidy the layout
The links are a compliance artifact an outside reviewer checks, but
nothing asserted they exist. A refactor of this layout could drop the
footer silently and the next platform submission would fail the same
check that prompted the PR. A feature test asserts the shared prop
reaches both guest screens and carries whatever the install configured,
and a browser test asserts the two links actually render to a logged-out
visitor.
Declares legal on SharedData, so the props read through the interface
rather than its index signature and the inline cast goes away.
Adds rel="noopener noreferrer" to both anchors, matching every other
target="_blank" in the codebase, and orders the imports the way eslint
expects so the file lands clean rather than relying on --fix in CI.
Drops the comment explaining why the links are there: that rationale
belongs in the commit and the pull request, and the sibling comments in
this file describe markup rather than justify decisions.
* Reuse the legal sentence the register screen already had
The register screen has shown "By continuing, you agree to our Terms of
Service and Privacy Policy" in production for a long time, translated
into all sixteen locales. Only the login screen was missing it, and the
two URLs were hardcoded inside the translated string, so a self-hosted
install could not point them at its own documents.
So this keeps what already worked and changes only those two things. The
markup moves into one component, which the login screen now renders as
well. The translated sentence keeps its wording and its link labels; only
the href becomes an i18n placeholder that the component fills from
config. That is one line per locale, and no new translation keys.
Reverting the footer out of AuthSplitLayout also stops the links from
appearing on the workspace index and create screens, which reach that
layout too and are seen after login rather than before it.
The sentence no longer hides on a self-hosted install. It was hidden
because it named TryPost's own documents; now that the URLs are
configuration, an install that sets them wants it shown.
A feature test covers the shared prop on both screens and the placeholder
in every locale; a browser test covers the rendered sentence, since the
links are a compliance artifact an outside reviewer checks and nothing
guarded them.
* Let the browser assertions do their own waiting
The test hand-rolled a polling loop in injected JavaScript to wait for
the element to mount, because the project notes say browser assertions do
not wait for SPA paint.
They do. visit() returns a PendingAwaitablePage backed by
AwaitableWebpage, whose __call wraps every method in
Execution::waitForExpectation and retries until the Playwright timeout,
which defaults to five seconds. The plugin even deprecates waitForText in
favour of assertSee for this reason. The loop was re-implementing the
retry that already surrounded each call, less well and with a helper
whose name has to be unique across the whole suite because these are
global functions.
Halves the file and drops the injected script. assertSeeLink also says
more than the old check did: it asserts the labels are links, not just
text that happens to appear.
---------
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
Signup already captures a click ID per ad network -- gclid, fbclid,
li_fat_id, ttclid, rdt_cid, epik -- but only the UTM parameters reached
the subscription. UTMs say which campaign a customer came from; the click
ID identifies the individual click, which is what Google and Meta need to
match a subscription back to the ad that produced it.
Every key is spelled out at the call site, so what reaches Stripe is
readable in one place without following a constant.
Click IDs are text columns on purpose, since truncating them at 255 would
destroy the value. Stripe caps a metadata value at 500 characters and
rejects the request rather than truncating, so an unbounded column
reaching it would fail the whole checkout and the customer could not
subscribe at all. Values are cut to 500 before they are sent. The UTM
columns are varchar(255) and cannot reach it; this exists for the text
ones.