- Removed the PostStatusGuard class and replaced its usage with the new PostStatusRules utility across multiple controllers and actions, enhancing code organization and maintainability.
- Updated error message handling to utilize the centralized method in PostStatusRules, ensuring consistency in user feedback.
- Deleted associated tests for PostStatusGuard, reflecting the removal of the class.
- Replaced direct status checks in multiple controllers and actions with the PostStatusGuard utility, improving code readability and maintainability.
- Updated error messages to utilize a centralized method for consistency across the application.
- Removed the BrandImagePalette class, consolidating color resolution logic into the AiImageClient for better organization and type safety.
- Improved type definitions for regeneration status and event handling, increasing clarity and maintainability.
- Streamlined the regeneration result handling by introducing a dedicated function to manage success and error states.
- Added computed properties to optimize form submission logic, ensuring better user experience and validation.
- Refactored the subscription logic to improve readability and error handling during media regeneration.
- Refactored the media item interface by moving it to a dedicated type file, enhancing code organization and reusability across components.
- Updated various components to import the new MediaItem type, ensuring consistency and reducing redundancy in type definitions.
- Improved the RegeneratePostMediaImage job by optimizing the media regeneration process and enhancing error handling for better reliability.
- Added guidelines for AI agents to avoid embedding prompts in PHP and to store them in Blade files under `resources/views/prompts/`.
- Updated `PostImageRegenerator` to render prompts using Blade instead of heredocs, improving maintainability and consistency with project standards.
- Enhanced documentation in `CLAUDE.md` and `.cursor/rules` files to reflect these changes.
Let users adjust AI-generated slides in place via async job and Echo, while applying workspace brand, background, and text colors to image prompts. Autofill swaps site text/background colors for the image palette, and regeneration is blocked on finalized posts with safer job cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
Refactor the user identification process by removing unnecessary type casting for user ID and account ID. This change enhances code readability and maintains functionality for syncing account usage.
Use Account::postsCountCacheKey everywhere, avoid findOrFail when syncing
post deletes, dispatch SyncAccountUsage after DeleteWorkspace when enabled,
and add coverage for the new paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Parse datetime-local values in the user timezone before converting to UTC,
matching formatUtcForDateTimeLocalInput. Fix ESLint import order in usePostCompliance.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Introduced a new translation key for future date selection prompts in English, Spanish, and Portuguese.
- Implemented logic to disable the confirm button and display a warning message when a past date is selected in the PickTimePopover component.
- Updated date formatting utility to handle UTC dates for HTML datetime-local inputs.
Pull the variant-snap branch out of togglePlatform into a named
helper with guard clauses. togglePlatform now reads top-down:
lock check → deselect short-circuit → snap → select.
Pulls ~200 lines of media/platform/length compliance and tooltip
derivation into composables/usePostCompliance.ts. Edit.vue is now
focused on state, save logic and event handlers; PLATFORM_VARIANTS
and the per-content-type media-rule check are exposed from the
composable for togglePlatform.
- Add separate canDelete predicate in Index.vue that includes Failed —
the previous EDITABLE_STATUSES gating hid the delete button for
failed posts even though the backend allows deleting them.
- Edit.vue Echo handler navigates to /show when an in-page real-time
status update transitions the post into a read-only state, instead
of leaving the user on a stuck readonly editor.
- FacebookPublisher: stop using empty() for content checks (treats
literal "0" as empty) — compare explicitly against null and "".
- Update terminal-state error messages in API + MCP update tool to
reflect the broadened guard (no longer Published-only). Adjust the
matching test assertion.
- Drop 'failed' from EDITABLE_STATUSES in Index.vue and Calendar.vue
so failed posts link directly to /show instead of /edit (which now
redirects).
- Replace remaining 'scheduled'/'publishing' literals in
PostEditorHeader.vue with the PostStatus enum.
- Include Failed in PostEditorHeader's READONLY_STATUSES to match
Edit.vue.
- Split PLATFORM_VARIANTS by Platform key (Platform.LinkedInPage and
Platform.InstagramFacebook were missing; page content types were
grouped under the wrong personal-LinkedIn key).
- PostController@edit was redirecting Failed→show while show was
redirecting Failed→edit, producing ERR_TOO_MANY_REDIRECTS. Failed
posts now render in show.
- New universal `hasContentOrMedia` rule in Edit.vue blocks publishing
when both text and media are empty (closes the hole where empty posts
could reach the publish button).
- Unified `PLATFORM_VARIANTS` to include Facebook, Instagram and
LinkedIn variants. togglePlatform snaps to a compatible variant when
reselecting a platform whose current content_type is incompatible
with the attached media (fixes the case where Reel+image left the
tile permanently blocked).
- platformIssues suppresses the issue on deselected tiles when a
compatible variant exists, so the tile remains clickable and the
snap can recover state.
- Use ContentType enum in place of string literals.
UpdatePost::execute returns Finalized for Published, PartiallyPublished,
Failed, and Publishing — the four "terminal" states the PR introduced.
The existing tests for UpdatePostTool (MCP) and Api/PostController only
exercised the Published path. Convert both to a Pest dataset over all
four statuses so a future regression that lets one state slip through
the check fails loudly.
Same shape already used for PublishPostTool and App/PostController.
- Remove cannot_edit_published from en/es/pt-BR posts.php — its sole
caller (App/PostController) was deleted in the previous commit
along with PostAction::AlreadyPublished.
- Add lang/php_*.json to .gitignore. They are build artifacts emitted
by laravel-vue-i18n/vite from the lang/{locale}/*.php sources and
shouldn't have been tracked (regenerated on every npm run build /
composer dev). Untracking now also means the orphan key disappears
from the compiled bundles on the next build without a manual edit.
UpdatePost::execute used to return AlreadyPublished for the Published
short-circuit. This PR widened the short-circuit to four terminal
statuses and consolidated them under PostAction::Finalized — so the
old enum case stopped being emitted, and every caller already had a
defensive in_array([AlreadyPublished, Finalized], ...).
Audit before removal: nothing emits AlreadyPublished anymore (only
UpdatePost::execute returns Actions, and it returns Finalized for
the whole terminal set), no test references the case, and no string
'already_published' exists elsewhere in app/resources/tests/lang.
- Drop the enum case
- Simplify the three in_array checks to a direct === Finalized
- Delete the dead App/PostController branch that flashed the old
cannot_edit_published message (its successor branch with
cannot_edit_finalized stays). The old i18n key is left in lang/
for now — orphan but harmless, can ressuscitate if a similar
flash is added back.
The earlier rename of UpdatePost's terminal short-circuit from
AlreadyPublished → Finalized was applied to UpdatePostTool and
Api/PostController but missed PublishPostTool. Result: calling
publish-post-tool against a post in Published / PartiallyPublished /
Failed / Publishing returned the unchanged post wrapped as a success
response — the MCP client thought it had republished.
The check now accepts both action enums so the existing string
behavior (clear error) is preserved, and a Pest dataset covers all
four terminal statuses.
Production incident: a customer's Facebook Page post failed with 'The post
is empty. Please enter a message to share.' (error code 197) and ended up
with a contradictory DB state (status=published + error_message=set).
Three independent bugs were uncovered:
A. FacebookPublisher sends 'message'/'description' as null when the user
posts media without text. Graph API requires the key be omitted, not
null. Fixed in publishSingleImagePost, publishMultiImagePost,
publishVideoPost, publishReel.
B. markAsPublished/markAsFailed leak stale fields across transitions
(a published row could retain error_message from a prior failure,
vice-versa). Both transitions now explicitly clear the opposite
side's fields.
C. status='failed' was editable in the UI and the backend, so users
were re-clicking Publish, generating duplicate failure emails and
the contradictory state from bug B. The frontend isReadOnly check
and the UpdatePost backend guard now treat Published/PartiallyPublished/
Failed/Publishing as terminal. To retry, the user duplicates the post.
11 new tests guarantee these can't regress silently: FB payload shape
per content type, PostPlatform field-clearing on transitions, and the
terminal-status block at the controller level.
Adds 'spaces' to config/filesystems.php so self-hosters can switch
FILESYSTEM_DISK=spaces without bolting onto the generic s3 disk. Env
vars follow the existing R2/AWS convention (SPACES_ACCESS_KEY_ID /
SPACES_SECRET_ACCESS_KEY), and use_path_style_endpoint is omitted so
the SDK defaults to virtual-hosted style — the canonical request shape
for DO Spaces.
Also documents the five env vars in .env.example. Docs in the
trypost-docs repo describe the matching FILESYSTEM_DISK=spaces stanza.
Members joining via workspace invite share the owner's account_id.
ProfileController::destroy was unconditionally calling $account->delete()
in every profile-deletion path, so any member could wipe the whole
organization (cascade: workspaces, posts, social accounts, signatures,
labels) just by clicking Delete on their own profile.
Gate the account/subscription teardown behind isAccountOwner(). For
members the path now only detaches them from workspaces and deletes
the user row — owner's data is untouched.
Tested in both SELF_HOSTED=true and false.
- POST /register with ?invite query and no prior session passes the gate.
- UserSeeder seeds the admin + workspace on an empty DB and skips when a
user already exists.
- Login page exposes selfHosted prop in both modes (proves the flag the
frontend uses to hide the "Sign up" link reaches the client).
Self-hosted installs (SELF_HOSTED=true, the default) now close /register
to the public. Workspace invites still work — the AcceptInvite page
links into /register with ?invite={id}, the middleware persists that
into the session, and POST /register passes through.
- EnsureRegistrationEnabled middleware gates GET/POST /register.
Accepts ?invite=… (URL) or pending_invite_id (session) as the pass.
- RegisteredUserController::store clears the marker after signup.
- AcceptInvite.vue passes invite.id in the register link's query string.
- Login.vue hides the "Sign up" link when self_hosted.
- UserSeeder bootstraps a single admin (admin@trypost.it / password).
Idempotent; not wired into DatabaseSeeder — operator runs
`php artisan db:seed --class=UserSeeder` per the install docs.
- Tests cover both flag values for every changed surface.
Docs PR: see trypost-docs self-hosting/installation.mdx step 3.
Audit found three behaviors with no explicit assertion:
- Job is dispatched with a 10-minute delay (would silently regress if
the duration changed). Uses Carbon::setTestNow + Bus::assertDispatched
inspecting \$job->delay.
- error_context.last_attempt_at is recorded at the moment of failure.
- updatePostStatus does NOT finalize the parent Post while any of its
platforms is in Retrying — covers the central invariant of the
feature (the post must stay Publishing until every platform lands
in Published or Failed).
- A platform currently in Retrying transitions to Published when the
next attempt succeeds — proves the loop terminates.
Before: a scheduled post hitting a platform outage was marked Failed —
user had to manually retry. Now the job reschedules itself for 10
minutes later and the PostPlatform shows status "Retrying". Loops
indefinitely until the platform accepts the post.
- Adds PostPlatformStatus::Retrying (existing string column, no migration)
- PublishToSocialPlatform: PlatformUnavailable catch now calls
rescheduleForRetry() which (a) updates the row to Retrying with
retry_count + next_attempt_at in error_context, and (b) dispatches
itself with a 10-minute delay. updatePostStatus() naturally leaves
the parent Post in Publishing because Retrying is neither Published
nor Failed.
- Same treatment for the retry-refresh edge case (publisher throws
TokenExpired, refresh subsequently fails with PlatformUnavailable).
- i18n + frontend status config updated (en, pt-BR, es) for both
posts.status.retrying and posts.edit.status.retrying.
- Tests: 3 new tests covering the dispatched job, the edge case path,
and retry_count increment across attempts.
Every refresh*Token method inside ConnectionVerifier already finishes
with \$account->update(...) (mutates the model) + \$account->refresh()
(reloads from DB to pick up sibling updates like LinkedInTokenSynchronizer).
The lock-contention branch in refreshToken() also calls \$account->refresh()
before returning.
So a second \$account->refresh() in the caller was always a redundant
SELECT — no scenario where it actually pulled a different value than
what CV already left in memory. Removed from all 15 call sites, plus
fixed the duplicated "Mastodon tokens don't expire" comment.
Edge case from the prior commits: if the publisher throws TokenExpired
(401 path), PublishToSocialPlatform attempts refreshAccountToken() to
recover. That internally goes through ConnectionVerifier::verify, which
can now raise PlatformUnavailable (5xx). The old catch (\Throwable)
swallowed it but the loop still fell through to markAsTokenExpired —
meaning a transient platform outage during a retry could still flip the
account to expired.
Adds an explicit PlatformUnavailable catch in the retry block: marks
the post failed with category platform_unavailable and breaks before
touching the account status.