Skip to content

fix(posts): block scheduling when content exceeds platform char limit - #26

Merged
paulocastellano merged 4 commits into
mainfrom
fix/post-action-blocks-on-content-overflow
May 11, 2026
Merged

fix(posts): block scheduling when content exceeds platform char limit#26
paulocastellano merged 4 commits into
mainfrom
fix/post-action-blocks-on-content-overflow

Conversation

@paulocastellano

Copy link
Copy Markdown
Contributor

Summary

  • Bug: AI-generated post with 537 chars was scheduled successfully to Facebook + Instagram + Threads, but the Threads publish job blew up with Content exceeds Threads limit of 500 characters (537 provided). The editor was already showing the 537|500 badge — but canSchedule ignored content length, so Schedule and Post Now stayed enabled. Backend UpdatePostRequest only capped content at 63206 (Facebook's max), not per-platform.
  • Fix: new App\Rules\ContentFitsPlatformLimits is applied to the content field in all three FormRequests (App\Post\UpdatePostRequest, Api\Post\UpdatePostRequest, Api\Post\StorePostRequest) via Rule::when(...), so drafts pass and schedules/publishes are rejected when any selected platform's hard cap is exceeded.
  • Frontend: Edit.vue feeds contentLengthOverflows into canSchedule, disabling the action buttons and surfacing each offending platform in postActionTooltip via the existing getPlatformLabel resolver.
  • De-duplication: added Platform::contentOverflow() as the single source of truth — reused from HasSocialHttpClient::validateContentLength (publish-time) so the schedule and publish paths cannot drift apart.

Test plan

  • vendor/bin/pint --dirty --format agent — passed
  • php artisan test --compact --parallel1504 passed, 2 skipped, 16199 assertions
  • npm run build — clean
  • New tests: 6 unit (ContentFitsPlatformLimitsTest) + 4 feature in UpdatePostRequestTest (App path) + 4 feature in PostApiTest (API store/update + draft-bypass)
  • Manual: open an AI-generated post with > 500 chars, attach Threads → confirm Schedule / Post Now disabled, tooltip lists the platform and overage
  • Manual: same post with all-text < 500 chars → buttons enabled

Out of scope

  • AI generator still targets one platformContext's recommended length and doesn't know about other platforms attached to the same post. If you cross-post AI-generated content to a platform with a stricter cap, the new validation will block at the schedule step rather than letting the user discover it on publish.
  • Platform::label() returns verbose names (Facebook Page, YouTube Shorts, Instagram (Standalone)). Frontend uses the cleaner getPlatformLabel map. Unifying is a wider refactor.
  • TikTok privacy check in withValidator is left as-is (pre-existing pattern, separate concern).

… limit

Threads posts over 500 chars were saved + scheduled successfully and only
failed inside the publish job. The frontend already showed the 537|500 badge
but `canSchedule` ignored content length, so Schedule and Post Now stayed
enabled. Backend `UpdatePostRequest` only capped at 63206 (Facebook's max),
not per-platform.

- Add `Platform::contentOverflow()` as the single source of truth and reuse it
  from `HasSocialHttpClient::validateContentLength` (publish-time).
- New `ContentFitsPlatformLimits` rule applied to the `content` field on
  `App\\UpdatePostRequest`, `Api\\UpdatePostRequest`, and `Api\\StorePostRequest`
  via `Rule::when(...)` so drafts are not blocked.
- Rule dedupes per platform (two Threads accounts -> one error) and reports
  the platform label, hard cap, and overage via i18n.
- Edit.vue feeds `contentLengthOverflows` into `canSchedule` and lists each
  offending platform in `postActionTooltip` using the existing
  `getPlatformLabel` resolver.
…flow

- Platform::contentOverflow returns int (0 = fits) via max(), drops the
  nullable-int + ternary pattern; HasSocialHttpClient and the validation
  rule updated to compare against 0 instead of null.
- Drop the hardcoded 63206-char limit to 10000 across all entry points
  (Platform enum, MCP CreatePostTool/UpdatePostTool, FacebookRules, all
  three FormRequests). Facebook's API accepts up to 63206 but nobody
  writes 63k-char posts and emoji-heavy content risks overflowing the
  TEXT column's 65535-byte ceiling.
- Compiled i18n JSON regenerated with the content_exceeds_platform key.
…t PostEditorHeader

Scheduled posts were silently editable: auto-save fired on every keystroke
with status='scheduled', and the backend either silently failed (per-platform
content-length, our new rule) or accepted invalid changes that blew up at
publish time. Editing a queued post is also a race with the publisher job.

UX change: scheduled state now reads-only the editor and exposes one
explicit escape — Unschedule to edit. The post drops to draft, user edits,
re-schedules. Matches Buffer/Hootsuite/Later/Postiz.

Implementation:
- New isLocked computed (= isReadOnly || isScheduled) gates save(),
  triggerAutosave(), debouncedSave(), togglePlatform(). submit() stays on
  isReadOnly so unschedulePost('draft') can still transition out.
- Editor body wraps in pointer-events-none + opacity-60 when isScheduled,
  visually dimming inputs without per-input :disabled plumbing.
- Header swaps to a violet "This post is scheduled / Unschedule to edit"
  banner replacing the entire normal action bar. Delete is hidden in
  this state — only escape is Unschedule, then act on the draft.

Refactor: extracted the entire header into PostEditorHeader.vue (matches
the PostEditorComposer / PostEditorSidebar family). Single <header> root
with v-if/v-else branches by status. ~85 lines out of Edit.vue.

Cleanup: dropped orphan posts.edit.scheduled_for translation key from
en/pt-BR/es — replaced by the new scheduled_overlay_* keys.
@paulocastellano
paulocastellano merged commit f217bba into main May 11, 2026
1 of 2 checks passed
@paulocastellano
paulocastellano deleted the fix/post-action-blocks-on-content-overflow branch May 11, 2026 23:57
paulocastellano added a commit that referenced this pull request May 12, 2026
Left over from the PostEditorHeader extraction in #26 — the header now
derives its own isPublished from the post prop, and Edit.vue itself
doesn't use it anywhere. ESLint caught it on CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant