fix(facebook): empty-message rejection + state consistency + no re-publish on terminal#41
Merged
Merged
Conversation
…blish on terminal 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.
665cf12 to
3f6032c
Compare
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.
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.
…ed JSONs
- 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 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.
- 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.
Introduce resources/js/types/post.ts mirroring App\Enums\Post\Status and App\Enums\PostPlatform\Status. Replace inline string comparisons across Edit.vue, Show.vue, PostEditorHeader.vue and ScheduleTab.vue.
- 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).
- 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.
The backend edit() controller already redirects posts in terminal states to /show; Inertia follows that redirect on partial reload.
…ance 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.
…helper 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.
…mePopover - 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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production incident
A customer's Facebook Page post failed with 'The post is empty. Please enter a message to share.' (FB Graph API error code 197), then ended up with a contradictory DB row: `status=published` + `platform_post_id=set` + `error_message='The post is empty...'`.
DB evidence on the failed PostPlatform:
```json
{
"category": "unknown",
"platform_error_code": "197",
"failed_at": "2026-05-15T13:34:24+00:00",
"content_length": 0,
"media_count": 2
}
```
Investigation surfaced three independent bugs that conspired to produce the customer experience.
Bug A — `FacebookPublisher` sends `message: null` to Graph API
When the user has media without text, `$content` is null. The 4 publish paths (`publishSingleImagePost`, `publishMultiImagePost`, `publishVideoPost`, `publishReel`) all included `'message' => $content` (or `'description' => $content`) in the payload regardless. Facebook expects the key to be omitted, not null.
Fix: conditionally include the key only when content is non-empty.
Bug B — `PostPlatform` state transitions leak stale fields
`markAsPublished` did not clear `error_message`/`error_context` from a prior failed attempt. `markAsFailed` did not clear `platform_post_id`/`platform_url` from a prior published attempt. This caused the contradictory `status=published + error_message=set` row.
Fix: each transition now explicitly nulls the opposite side's fields.
Bug C — failed posts are still re-publishable
`Edit.vue::isReadOnly` did not include `'failed'`, so the UI re-enabled the Publish button after a failure. The customer (or anyone) could re-click and:
Fix:
To retry a failed post, the user duplicates it (existing flow).
Tests (11 new — regression guarantee)
If anyone removes the empty-message guard, the field-clearing, or the terminal block in the future, the build fails before merge.
Test plan
Out of scope