Commit graph

89 commits

Author SHA1 Message Date
Paulo Castellano
f4fd8a2aab refactor: centralize social API base URLs into the configuration file to improve maintainability and environment flexibility 2026-05-02 13:49:20 -03:00
Paulo Castellano
8ba5877bdf feat: implement automated brand color extraction from web metadata, CSS, and logos with supporting database schema updates 2026-05-02 13:30:59 -03:00
Paulo Castellano
f3605717c7 refactor: unify social analytics, reorganize workspace settings, and implement content validation rules 2026-05-02 12:22:42 -03:00
Paulo Castellano
3c3b170b21 feat: @mentions in comments, AI Action layer + MCP tools, settings tabs
Mentions in post comments
- @mention autocomplete (workspace members, current user excluded) with
  marker syntax @[uuid] persisted, display names rendered via CommentBody
  chips; live edit replaces markers with names and converts back on save.
- NotifyMentions action with workspace-scoped membership check, dedupes
  same user, only newly-added mentions on update.
- Email + in-app via SendNotification job, respecting per-user
  notification_preferences.mentioned_in_comment.
- Heartbeat-based presence (Cache, 60s TTL, 30s ping) so online recipients
  get only the in-app notification — no email noise.
- Real-time bell on workspace.{id}.user.{id} private channel
  (NotificationCreated event), scoped channel name avoids client-side
  filtering and lays out a convention for future workspace channels.
- Mailable localized via lang/{en,es,pt-BR}/mail.php; Maizzle source
  template for the email is committed and built into resources/views/mail.

AI generation refactor (Action layer + MCP)
- Extracted Actions/Ai/Generate{Image,Video} with QuotaExhaustedException
  so agent tools and MCP tools share a single domain entry point.
- Mcp/Tools/Ai/Generate{Image,Video}Tool registered in TryPostServer; both
  return MediaResource payloads.
- Orientation::imageApiSize maps non-OpenAI ratios to 1:1/2:3/3:2.
- config/ai.php is now the single source of truth driven by env, removing
  the trypost.ai shim. Default text/image providers flipped to OpenAI.

Settings/UX
- /settings/workspace split into shadcn Tabs (Workspace / Brand / Users)
  with three components.
- /assets and the in-editor MediaPicker open the ImagePreviewDialog
  lightbox on image click while preserving action button behaviour.
- Comments tab landed via ?tab=comments&comment=<id> from notification
  click (scroll-to + temporary highlight).
- Mention autocomplete popover flips above when near the viewport bottom.
- Real social platform PNGs replace Tabler brand glyphs in schedule
  pills and post list, with hover tooltip carrying display_name + handle.

Bug fixes
- AcceptInvite: controller now passes workspace + role payload that the
  Vue page expects; login/register CTAs preselect the invite email.
- WorkspaceInvite mailable: stopped referencing nonexistent
  $invite->workspace and $invite->role; column added to the migration,
  Invite model casts role to WorkspaceRole, CreateInvite persists it.
- PostCommentCreated: added broadcastAs so .PostCommentCreated actually
  matches the Echo listener; payload now includes mentioned_users so
  receivers render the chip correctly without a refetch.
- Preview components for X/Pinterest/Threads/Bluesky/LinkedIn/Mastodon/
  TikTok/YouTube switched from item.type === 'image' to
  !isVideoMedia(item) so media without a persisted type still renders.
- UpdatePostRequest now accepts media.*.{type,mime_type,size,...} so the
  posts.media JSON keeps the metadata that the previews need.
- Removed throttle:6,1 from social connect routes (was 429ing legitimate
  OAuth retries).
- Used MediaType enum cases instead of literal 'image'/'video' strings
  when creating media rows.

Tests
- MentionParser unit tests, NotifyMentions feature tests including
  online/offline channel selection and preference gating, MCP AI tool
  happy paths, MentionedInComment mailable rendering, AcceptInvite +
  search-members + index mentioned_users path. 1229 passing.
2026-05-01 20:59:03 -03:00
Paulo Castellano
dafdd5da43 feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
  GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
  post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
  EmojiPicker (full Unicode set, search, categories, recently-used,
  light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
  content_types map) so the user can switch Feed/Reel/Story etc. and have
  it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
  types); replaces inline maps across 5 components, fixes
  instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
  platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
  on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
  (was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 14:53:49 -03:00
Paulo Castellano
35646bbaf6 chore: working 2026-04-23 13:23:24 -03:00
Paulo Castellano
b3b59b4d13 refactor: remove onboarding flow, implement brand analysis services, and replace setup middleware with account readiness checks 2026-04-16 23:05:51 -03:00
Paulo Castellano
bde10059e3 feat: add brand autofill from website URL in onboarding
Users on the Brand step can type their website URL and click
'Preencher' (Portuguese) / 'Autofill' (English). The backend fetches
their homepage, parses standard meta tags, and returns:

- name        ← og:site_name | title (suffix stripped at ' | ', ' - ')
- description ← meta[name=description] | og:description
- language    ← <html lang> mapped to en / pt-BR / es
- logo        ← apple-touch-icon | largest link[rel*=icon] | og:image

The logo is downloaded, validated (mime whitelist, 2MB max), and
attached to the workspace's 'logo' media collection so the avatar
updates immediately.

Zero LLM calls, zero external APIs. Uses symfony/dom-crawler +
symfony/css-selector (newly required) for meta extraction. Everything
else (Http client, workspace media, Intervention) was already in the
project.

Security:
- SSRF guardrail: resolves the host, rejects private / loopback /
  link-local ranges, enforces http(s) scheme on both the initial
  page fetch and the logo download.
- Rate limited at 10 req/min per user via the throttle middleware
  alias on the route.
- Logo content-type must be one of the allowed image mimes; wrong
  types are silently dropped so users never see broken images.

UX:
- 'Autofill' button next to the website input, disabled until there
  is a URL; shows a spinner while running.
- If a logo was captured, a small preview appears below the input
  so users can see what was pulled before saving.
- Success and error paths both surface as vue-sonner toasts, with
  translations in en / pt-BR / es.
- Failures leave the form untouched — nothing is destructively
  overwritten if parsing gave us nothing.

Tests (16 new): action-level coverage for happy path, title-suffix
fallback, language code normalization across 6 locales, scheme
rejection, private-range SSRF rejection, implicit https prefixing,
empty sites, upstream errors, and wrong-mime logo rejection. Plus
two controller-level tests for the autofill endpoint.
2026-04-16 11:13:54 -03:00
Paulo Castellano
287c27b792 feat: add brand configuration step to onboarding
After users pick their persona (role), they now land on a new Brand
step that collects the same fields available in Settings → Workspace
→ Brand: website, description, tone, voice notes, and content
language. When they continue, every AI-generated post for this
workspace already has sensible defaults — before the user's first
post is ever drafted.

Flow:
Role (persona) → Brand (new) → Connections → Subscription → Completed.
A 'Skip for now' button on the brand step advances to Connections
without touching the workspace (defaults stay at their seed values).

Backend:
- Setup enum gets a new Brand case slotted between Role and
  Connections with matching stepNumber updates.
- OnboardingController::brand() renders the form pre-filled from the
  current workspace. storeBrand() validates via a new
  StoreBrandRequest form request and writes the fields onto the
  workspace, then advances setup. skipBrand() just advances.
- storeRole() redirects to brand instead of account. enforceStep()
  knows how to redirect users whose setup is Brand.
- Three new routes: GET /onboarding/brand, POST /onboarding/brand,
  POST /onboarding/brand/skip.

Frontend:
- New Brand.vue page mirrors the Settings brand form but inside the
  onboarding AuthLayout. Tone + language sit side by side, both
  selects take full width. Translations added to en, pt-BR, and es.
- Wayfinder regenerated so the page can import storeBrand / skipBrand.

Tests:
- Renamed 'redirects to step2' → 'redirects to brand step' and
  assert new setup.
- Added six new tests covering brand step auth, redirects, render,
  successful store, validation of tone and content_language, and
  skip.
- Updated UserSetupTest for the new enum case + reshuffled step
  numbers.
2026-04-16 11:00:16 -03:00
Paulo Castellano
5f7768ca3a fix: apply code review — enforce quota server-side, project conventions
Critical fixes:
- Quota enforcement regressed when the regex flow was replaced with
  tools. The LLM was trusted to respect [Session state] quota hints,
  which is vulnerable to prompt injection / hallucination. Each tool
  now checks Pennant feature limits at the top of handle() and returns
  a short quota-exhausted string instead of calling the provider.
  Audio shares the video quota (there is no AiAudioLimit by design).
- N+1 on aiMessages: combined the two separate ->get() scans that
  produced imagesInThread and videosInThread into a single query.
- postPlatforms lazy load: controller now loadMissing('postPlatforms')
  before constructing SocialMediaAssistant, which accesses the relation
  in activePlatformRules().

Project conventions:
- \RuntimeException and \Throwable are now imported at the top of the
  controller instead of inlined per CLAUDE.md.
- Tool handle() methods use data_get($request, 'prompt') instead of
  direct array access, matching the data_get convention for service
  classes.
- enrichContent() pluralizes attachment types via Str::plural so
  '2 images' reads naturally instead of '2 image'.

Tests:
- Added Storage::fake('public') in tool tests so runs don't pollute
  the local disk with fake-generated files.
- Restored quota enforcement coverage (deleted when the flow changed)
  as three new tests — one per tool — that fill the monthly bucket
  and assert the tool refuses and nothing is generated.
- Removed inline \App\ references in tests in favor of imports.
2026-04-16 10:22:55 -03:00
Paulo Castellano
e3f80b91d4 refactor: migrate PostAssistantController to SDK Agent + tools
Controller now delegates to SocialMediaAssistant::prompt() and reads
generated attachments from AttachmentCollector (request-scoped). The
three preg_match branches for [GENERATE_IMAGE/VIDEO/AUDIO] commands
are gone — the LLM invokes tools directly with typed parameters.

Deleted:
- app/Services/Ai/GeminiTextGenerationService.php
- app/Services/Ai/TextGenerationService.php  (OpenAI alternative)
- app/Services/Ai/ImageGenerationService.php
- app/Services/Ai/AudioGenerationService.php
- app/Services/Ai/Contracts/TextGenerationInterface.php

Kept: VideoGenerationService (wrapped by GenerateVideo tool since
Veo is not in the SDK's provider matrix) and IntentDetector.

Tests now fake the agent via SocialMediaAssistant::fake() with either
canned text or a callable that simulates tool side-effects by pushing
directly into AttachmentCollector.
2026-04-16 09:59:10 -03:00
Paulo Castellano
8adbee6276 fix: update monthlyCount() callers to pass UsageType enum
AiUsageLogTest and UsageController still passed string 'image'/'video'
arguments to monthlyCount() after the enum migration. Update them to
pass UsageType::Image and UsageType::Video.
2026-04-16 09:29:37 -03:00
Paulo Castellano
b2bf5c2059 feat: complete AI assistant with custom services and brand config
Baseline snapshot of custom AI implementation before Laravel AI SDK migration.

Includes:
- Custom services: GeminiTextGenerationService, TextGenerationService (OpenAI), ImageGenerationService, AudioGenerationService, VideoGenerationService
- IntentDetector for content moderation via keyword matching
- AI enums: Intent, Orientation, UsageType
- Blade prompt templates: system.blade.php, image.blade.php, video.blade.php
- AiMessage with content_html accessor (markdown rendering)
- AiUsageLog for monthly quota tracking per account
- PostAssistantController with regex-based [GENERATE_*] parsing
- WritingAssistantTab with markdown rendering, add-to-post, attachments
- Workspace brand fields (name, description, tone, voice_notes) in system prompt
- Session state block injected into prompts (thread counts, quota remaining)
- AttachmentCollector pattern will replace the regex approach in Phase 2
- Post comments with replies, emoji reactions, real-time via Echo
- Assets page with Unsplash + Giphy integrations
2026-04-16 09:25:08 -03:00
Paulo Castellano
5493871b2d feat: add brand config to workspace and Blade prompt system for AI 2026-04-15 21:28:40 -03:00
Paulo Castellano
baf310112e fix: add limit checks, rate limiting, error styling, and test fixes for AI assistant 2026-04-15 21:23:13 -03:00
Paulo Castellano
762fa77583 feat: add AI generation services and usage tracking 2026-04-15 21:06:06 -03:00
Paulo Castellano
ced8bc818b feat: add CommentsTab component with replies, reactions, and real-time 2026-04-15 20:15:29 -03:00
Paulo Castellano
0f6ae9a4e6 feat: add PostCommentCreated broadcast event 2026-04-15 20:11:36 -03:00
Paulo Castellano
e3d14256e0 feat: add PostCommentController with CRUD + reactions routes 2026-04-15 20:10:45 -03:00
Paulo Castellano
e1965f9f7c feat: integrate Giphy support and add trending endpoints for media assets 2026-04-15 11:02:10 -03:00
Paulo Castellano
0d88ced20b feat: implement asset management system with Unsplash integration and chunked uploads 2026-04-15 10:20:37 -03:00
Paulo Castellano
a8bb44f0e5 fix: self-host billing bypass, Facebook API metrics migration, X token refresh, and duplicate social accounts
- Block all billing/usage/subscribe routes in self-hosted mode (redirect to calendar)
- Hide billing_email field in account settings when self-hosted
- Migrate deprecated Facebook Page Insights metrics to new Media Views API (v25.0)
- Fix X analytics token refresh to use Basic Auth (matching XPublisher/ConnectionVerifier)
- Prevent duplicate social accounts by using updateOrCreate across all OAuth controllers
2026-04-15 09:46:18 -03:00
Paulo Castellano
ded1c998ec feat: redesign billing, onboarding, sidebar, and settings architecture
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
  API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
  -> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
  trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
  sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
  activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
  notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
2026-04-15 00:33:38 -03:00
Paulo Castellano
2da15df96c feat: introduce Account entity as billing owner and refactor architecture
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
  WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
  enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
2026-04-14 22:22:04 -03:00
Paulo Castellano
8caa3d55c7 feat: redesign accounts page with multi-account support
- New card-based layout showing connected accounts with avatar, platform
  badge, brand label, and connection date
- "Add Social" button opens dialog with platform grid for connecting
- Removed unique constraint on workspace_id+platform to allow multiple
  accounts per platform
- Removed "already connected" checks from all 13 OAuth controllers
- Updated tests to verify multi-account connection works
2026-04-14 19:48:35 -03:00
Paulo Castellano
e876ba3839 fix: wire inviteMember policy in invite controller and add brand translations 2026-04-14 19:14:41 -03:00
Paulo Castellano
c867faa2ee fix: address code review issues for billing migration 2026-04-14 18:44:47 -03:00
Paulo Castellano
1ff2f93c37 feat: move Billable from User to Workspace with plan support 2026-04-14 18:22:36 -03:00
Paulo Castellano
c107ce3949 feat: create brands with CRUD, policy, and tests 2026-04-14 18:04:41 -03:00
Paulo Castellano
4cf601e618 feat: add YouTube Analytics with 7 channel metrics
Integrates YouTube Analytics API v2 to display channel-level metrics
(views, minutes watched, avg view duration, avg view percentage,
subscribers gained/lost, likes) with date range support and caching.
2026-04-14 13:10:27 -03:00
Paulo Castellano
0e930d7bb2 fix: add /me API call in Facebook OAuth callbacks to trigger public_profile for Meta app review 2026-04-03 21:21:53 -03:00
Paulo Castellano
cb91529964 chore: working 2026-04-02 17:57:06 -03:00
Paulo Castellano
1129205c22 fix: TikTok add missing scopes + send empty body to creator_info endpoint 2026-04-01 15:33:17 -03:00
Paulo Castellano
66d0731090 fix: YouTube requires content text — frontend validation now shows error when content is empty 2026-04-01 15:12:56 -03:00
Paulo Castellano
146bd7b7d2 fix: delete post redirects to posts index instead of back() which causes 404 2026-04-01 15:04:13 -03:00
Paulo Castellano
0bca140cd9 fix: API scheduled_at validation, redirect allowlist, UUID model_id, UpdatePost transaction, safe resolveModel 2026-04-01 13:21:22 -03:00
Paulo Castellano
5e1f9eaa82 refactor: move duplicate validation to DuplicateMediaRequest FormRequest 2026-04-01 13:01:07 -03:00
Paulo Castellano
29bc7deb21 fix: restrict status to user-settable values, validate duplicate targets, scope API labels 2026-04-01 12:57:43 -03:00
Paulo Castellano
9d0a860d87 fix: prevent double-publish race condition, media upload auth bypass, TokenExpired disconnected_at 2026-04-01 12:34:38 -03:00
Paulo Castellano
f3c7a3bc13 fix: scheduled_at after:now validation, Facebook token not exposed to frontend, duplicate notifications, chunked mime validation 2026-04-01 12:19:24 -03:00
Paulo Castellano
9f3b8e547a fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
  (content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
  — X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
  timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 19:25:19 -03:00
Paulo Castellano
c020419db1 feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 01:42:39 -03:00
Paulo Castellano
74c6442728 refactor: code review fixes — policies, enums, data_get, tests
- Refactor WorkspacePolicy to use pivot role instead of workspace.user_id
- Add manageBilling policy (owner only) to BillingController
- Fix ApiKeyController authorization (view → manageTeam for store/destroy)
- Fix WorkspaceInviteController using workspace.user_id for owner checks
- Fix WorkspaceController settings is_owner using workspace.user_id
- Create PostAction enum for UpdatePost/PostController action strings
- Create ApiToken\Status enum
- Add User::SUBSCRIPTION_NAME constant, replace all hardcoded 'default'
- Convert wantsEmailFor to accept NotificationType enum
- Convert all $data[] to data_get() across publishers, controllers, jobs
- Fix SocialLoginController callback missing try/catch
- Fix SocialController::toggleActive missing workspace null check
- Fix UpdatePost NPE on meta merge when postPlatform not found
- Remove HTML5 required attributes from form inputs
- Convert function declarations to arrow functions in Vue components
- Replace hardcoded URLs with Wayfinder route helpers
- Replace new Date() with dayjs
- Add 16 new test files covering policies, authorization, publishing
2026-03-31 00:40:18 -03:00
Paulo Castellano
7d95fd1efa chore: tracking, gtm and more 2026-03-30 21:32:43 -03:00
Paulo Castellano
843b3991ec chore: posthog, ui, features and more 2026-03-30 21:18:07 -03:00
Paulo Castellano
aa7ca8ae21 refactor: notification preferences, header slots, calendar layout, UI polish
Notification preferences:
- Create notification_preferences table (post_published, post_failed,
  account_disconnected booleans per user)
- NotificationPreferenceController with firstOrCreate on first visit
- SendNotification job respects email preferences before sending
- Settings page with toggle switches, i18n in 3 languages
- 8 new tests for preferences (controller + wantsEmailFor + job integration)

Post published notification:
- PostPublished mail + maizzle template
- Notify owner on successful publish via SendNotification job
- PostPublished type added to notification enum

Header & Layout:
- Rename AppSidebarHeader to AppHeader with left/center/right slots
- showSidebarTrigger prop to hide sidebar toggle
- Calendar: controls in header (left: nav, center: date, right: tabs + new post)
- Fixed header with scrollable content (flex h-screen pattern)
- fullWidth pages use overflow-y-auto (fixes month view scroll)

UI improvements:
- Action buttons moved to header-right: posts, hashtags, labels
- Settings breadcrumbs: "Settings > Profile" pattern
- Calendar: remove duplicate New Post button from day view
- Remove size="sm" from Schedule/Publish buttons
- Remove bg-background from header (inherits from SidebarInset)
- Add Cancel button to labels and hashtags create/edit dialogs
- Add common.cancel i18n key
- Clean up orphaned Calendar breadcrumbs
- Fix SocialAccountsGrid buttons to use shadcn Button ghost

All 753 tests passing.
2026-03-30 18:18:17 -03:00
Paulo Castellano
09e37f2879 feat: notification system with SendNotification job, dialog UI, tests
Backend:
- Create notifications table (user_id, workspace_id, type, channel,
  title, body, data JSON, read_at, archived_at)
- Create Notification model with Type enum (post_failed,
  account_disconnected, invite_received, member_joined, member_removed)
  and Channel enum (email, in_app, both)
- Create SendNotification job: isolated from publish flow, handles
  saving in-app notification and sending email independently
- NotificationController: index (excludes archived, scoped to workspace),
  markAsRead, markAllAsRead, archiveAll
- Integrate with PublishToSocialPlatform (post failed/partial)
- Integrate with VerifyWorkspaceConnections (batch disconnection)
- Integrate with SocialAccount::markAsDisconnected (single disconnection)
- All use SendNotification::dispatch() instead of direct Mail::to()

Frontend:
- NotificationBell component in sidebar footer with unread badge
- Dialog with notification list, mark as read, mark all read, archive all
- Click navigates to relevant page (post edit, accounts)
- i18n for notifications UI (en, es, pt-BR)

Tests:
- 8 tests for NotificationController (auth, CRUD, workspace scoping)
- 4 tests for SendNotification job (channels, email, data storage)

All 745 tests passing.
2026-03-30 16:47:03 -03:00
Paulo Castellano
ceb7b92b74 feat: PostPlatform enum, failure email, DB indexes, rate limiting, tests
Publishing improvements:
- Create PostPlatformStatus enum (Pending, Publishing, Published, Failed)
- Update PostPlatform model, jobs, factories to use enum
- Add PostPublishFailed email notification when post fails to publish
- Maizzle template + blade for failure email with platform details
- PublishPost job: add $tries=3, $backoff=30, failed() method
- Fix broadcast event to serialize enum status value

Security:
- Add rate limiting (throttle:6,1) on social connect endpoints
- Fix MediaController::reorder IDOR vulnerability
- Fix Connect.vue broken import (storeStep2 -> storeConnect)
- Fix UpdatePost data_get() consistency

Database:
- Add composite index on post_platforms (post_id, enabled)
- Add index on post_platforms (social_account_id)

Tests:
- Add 3 tests for profile photo upload/delete
- Add 2 tests for media reorder (including IDOR check)
- Fix publish tests for PostPlatformStatus enum
- Add Mail::fake() to publish tests

Cleanup:
- Remove unused AppHeader.vue and AppHeaderLayout.vue
- Remove dead BillingController methods

All 733 tests passing.
2026-03-30 16:11:38 -03:00
Paulo Castellano
06e01797d1 fix: security audit - IDOR, open redirect, authorization, session fixes
Critical:
- Fix EnsureUserSetupIsComplete middleware route name prefixes and
  redirect Subscription step to subscribe page (not onboarding)
- Fix MCP session pollution: Auth::setUser() instead of Auth::login()
- Remove dead BillingController::addWorkspace/removeWorkspace methods
- Remove broken Workspace::pendingInvites() method

Security (IDOR):
- MediaController: add workspace ownership verification on all endpoints
- UpdatePostRequest: scope label_ids validation to current workspace
- UpdatePostRequest: scope platform IDs validation to current post

Security (other):
- Fix open redirect in login and registration (validate internal URLs)
- Add validation to API PostController store/update (was $request->all())
- Prevent Owner role assignment via updateRole endpoint
- Fix API post author attribution to use workspace owner

Authorization:
- PostController: use createPost policy instead of view for store/update/destroy

Logic:
- Post Status enum labels now use translation system instead of hardcoded Portuguese
- Workspace deletion cleans up current_workspace_id for all affected members
- StoreWorkspaceInviteRequest: replace Portuguese validation messages with __()

Rename onboarding:
- Step1.vue -> Role.vue, Step2.vue -> Connect.vue
- Controller methods: step1->role, storeStep1->storeRole, step2->connect, storeStep2->storeConnect

All 728 tests passing.
2026-03-30 14:58:25 -03:00
Paulo Castellano
f3f72afecd refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale
Auth pages:
- Create AuthSplitLayout with animated feature slides (6 slides, 3 languages)
- All auth pages use split layout (form left, visual right)
- Add show/hide password toggle with tooltip on Register
- Legal footer only shown on Register via showLegal prop

Subscribe page:
- Redesign to match auth card pattern (centered, clean)
- Platform icons, feature checklist, dynamic trial days (trialDays - 1)
- Add "Switch workspace" link
- Full i18n (en, es, pt-BR)

Onboarding:
- Rename URLs: step1 -> role, step2 -> connect
- Add enforceStep() to prevent skipping/going back steps
- Redirect /onboarding to /onboarding/role
- Redesign Step2 with AuthSplitLayout and compact platform list
- 21 tests covering all step enforcement scenarios

Workspaces page:
- Redesign with AuthSplitLayout (list with avatars, current badge)

Language system:
- Move locale from DB to cookie (forever, unencrypted, session.domain)
- Create SetLocale middleware (sets cookie if missing, validates against config)
- Rename lang/pt-br to lang/pt-BR
- Add dayjs es locale

Other:
- Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard)
- ConfirmDeleteModal with text confirmation (sendkit pattern)
- i18n for ConfirmDeleteModal internal strings (common.php)
- EmptyState component for posts index
- Exact match for "All" posts in sidebar
- Posts breadcrumbs show current status filter
- DialogFooter buttons aligned left
- API Keys page redesign with Table, DropdownMenu, EmptyState
- Extract CreateApiKeyDialog and InviteMemberDialog to components
- Remove API Keys from sidebar
- DropdownMenuItem destructive variant for Remove action
2026-03-30 11:53:42 -03:00