Default to web when the value is missing, null, or invalid so post creation never fails over provenance.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject CreatePost calls without CreatedVia, set Web on DuplicatePost, and assert wiring for templates, AI, automation, and API spoof attempts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Persist whether a post was created through web, MCP, API, or automation so we can attribute entry points without guessing from request context.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use GET /2/media/upload?media_id=&command=STATUS per X API docs, and persist
redacted raw_response on SocialPublishException failures for supportability.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fail hard when media processing fails, map media-specific X invalid-request
errors clearly, and cover GIF/large-image/amplify/finalize/append failure paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
X rejects empty finalize requests with "Request body must be a JSON object",
breaking video and large-media posts that use the chunked upload flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replaced `vendor/bin/sail` with `php` in documentation to unify command instructions.
- Updated `.mcp.json` and `.cursor/mcp.json` to reflect changes for Laravel Boost.
- Enhanced clarity in skill documentation for Cashier, Horizon, MCP, Passport, Pest, and Wayfinder.
- Disabled Sail support in `boost.json` and introduced "codex" as a new agent.
- Replaced instances of `vendor/bin/sail` with `php` in various documentation files to standardize command usage.
- Updated `.mcp.json` and `.cursor/mcp.json` to reflect the new command for Laravel Boost.
- Adjusted skill documentation files for Cashier, Horizon, MCP, Passport, Pest, and Wayfinder to enhance clarity and ensure proper command execution.
- Disabled Sail support in `boost.json` and added "codex" as a new agent.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Changed command from `php artisan` to `vendor/bin/sail artisan` in various documentation files to ensure consistency with the use of Laravel Sail.
- Updated `.mcp.json` to reflect the new command for Laravel Boost.
- Added new agent "cursor" in `boost.json` and enabled Sail support.
- Updated PHP version in `CLAUDE.md` from 8.4 to 8.5.
- Made minor adjustments to various skill documentation files to enhance clarity and ensure proper command usage.
Co-authored-by: Cursor <cursoragent@cursor.com>
YouTube Shorts now allow videos up to 3 minutes. The dashboard media rules still capped uploads at 60s (API/MCP already had no duration gate), so align the frontend validation, content-type copy, and unit test.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds dedicated tests for local assemble, multipart complete/progress, MediaResource response shape, and orphan R2 cleanup when media registration fails.
Co-authored-by: Cursor <cursoragent@cursor.com>
The uploads grid is client-fetched, so an Inertia delete with preserveState left the stale list on screen until a hard refresh.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the image-normalize vs binary-stream split, but move each path into a focused private method so addMediaFromPath stays linear.
Co-authored-by: Cursor <cursoragent@cursor.com>
Controller only authorizes and returns ChunkReceipt. Multipart vs local assemble lives in ChunkedAssetReceiver; JSON shape comes from MediaResource.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix real edge cases: enforce 5MiB non-final parts, track offsets for ordered/idempotent chunks, size from bytes received, delete orphaned R2 objects if Media create fails, and split the controller into clearer paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Dataset for multipart strategy selection plus HTTP paths for assemble (local/public/images) and multipart (s3/r2/spaces videos).
Co-authored-by: Cursor <cursoragent@cursor.com>
Local/public already used the assemble-then-store flow. Rename supports() so it is obvious every filesystem disk works; multipart only kicks in for S3-compatible remote disks.
Co-authored-by: Cursor <cursoragent@cursor.com>
The last chunk was reassembling the full file locally and pushing ~185MB to R2 in one request (53s+). Videos/PDFs now upload each chunk as an S3 multipart part so finalize only completes the upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
Unicode filename encoding was correct, but large videos still died on the last chunk: the whole file was loaded into memory and uploaded to R2 via Guzzle within PHP-FPM's 30s limit. Stream non-images with writeStream and lift the time limit on finalize.
Co-authored-by: Cursor <cursoragent@cursor.com>
X-File-Name is an HTTP header, so raw en-dashes/emoji blow up fetch() before the request leaves the browser. Percent-encode on the client and rawurldecode on the server.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a 1200x630 changelog thumbnail as a fourth release artifact, generated
from a shared brand template that mirrors the marketing-site hero / OG image:
warm cream to lavender wash, ink dot-grid, an Instrument Serif headline with a
hand-drawn violet squiggle, a segmented 'CHANGELOG | <version>' sticker badge,
and colored ink-bordered theme chips with solid offset shadows.
- .claude/release-assets/: render-thumbnail.mjs (Playwright renderer),
thumbnail.template.html (brand template), logo.png (TryPost wordmark)
- .claude/commands/release.md: new Step 5b renders the thumbnail; it is
versioned in the artifacts PR alongside the changelog and email
- releases/v1.0.6/thumbnail.png: image for the latest release
Adds a single-select referral-source step between the goals and connect
steps of onboarding. The choice is stored on users.referral_source and
mirrored to PostHog, mirroring the existing persona and goals steps.
- ReferralSource enum (12 sources) + nullable users.referral_source column
- referralSource()/storeReferralSource() controller actions with the same
self-hosted, subscribed, persona and goals guards as the sibling steps
- connect() now requires a referral source before rendering
- Single-select ReferralSource.vue page mirroring the goals step
- Localized across all 15 locales
Resolves the AiPostWizard conflict and completes the feature:
- i18n parity: brand_colors_label + brand_colors_description in all 15 locales
(was en/es/pt-BR only, which broke LocalizationParityTest).
- Reworked the two-button toggle into a Switch with an explanatory description
(matches the settings Switch/card pattern).
- Only shown for templates that honor the flag: added appliesBrandVisuals() to
the AiContentTemplate contract (ImageCard=true, tweet cards=false), exposed as
applies_brand_visuals in the create-page DTO, and gated the toggle on it — so
it no longer appears (as a no-op) for tweet-card styles.
- Tests: TemplateContractTest covers appliesBrandVisuals for all templates.
The workspace switcher now opens to the right on desktop (like the user menu)
and stays below the trigger on mobile, via useSidebar()'s isMobile. The trigger
icon follows the direction: chevron-right on desktop, an up/down selector on
mobile — for both the workspace switcher and the user menu.
The scheduled-post banner in the editor now clears the floating sidebar
trigger on mobile (pl-12 md:pl-0), aligns its calendar icon to the top of
the text, and matches the hamburger's size (size-10 md:size-9). Desktop is
unchanged.
- automations: minimal back-only header on mobile for the workflow builder
and detail tabs (extracted AutomationMobileBackHeader); open live
automations on the metrics tab (drafts still open on workflow); full-width
status filter + refresh on the invocations toolbar; use IconMenu2 for the
mobile sidebar trigger
- dialogs: stack DialogFooter primary-on-top / cancel-at-bottom on mobile
- workspaces: bring the workspace picker cards into the neo-brutalist design
- posts: left-align the label filter content; wrap the post-view date/status
header so a long status badge no longer squeezes the date; add hamburger
clearance to the editor's mobile tab bar
It's only the autosave indicator (Draft/Saving/Saved) and it crowded the tabs.
Drop it from the mobile switcher (the desktop header still shows it) so the tabs
get the full bar.
Match the switcher tab height to the sidebar trigger (h-10), and let the primary
publish button fill the footer (flex-1) on mobile while staying auto-width on desktop.
On mobile the header was a whole bar showing only the Draft/Saving/Saved status.
Move that status into the switcher bar (right side) and hide the header on mobile
(the scheduled banner still shows), reclaiming a row. The switcher clears the
floating hamburger when it's the top bar.
Stack it into a clean back-button row and a date + status row (date left, badge
right) instead of cramming everything into one line where the date wrapped and
the badge floated.
Move the floating hamburger from left-2/top-2 to left-4/top-3 so it lines up
with the header's content padding (and the calendar's nav buttons); bump the
non-header page top reserve to pt-14 to clear the taller trigger.