Add a Publisher abstraction and queued job to publish from the UI

Introduce Publisher/PublisherManager (WordPress + Reddit implementations)
so publishing logic is shared between the CLI, the queue, and future
network integrations. Add a "Publish" button on the Posts page that
dispatches PublishPostJob instead of blocking the request — Reddit's
publisher shells out to the existing Playwright sidecar and can take
tens of seconds, so it can't run synchronously in a web request.

Adds a `target` field (e.g. subreddit) and `publishing` status to Post,
plus a `publish_error` field so failures surface in the UI. Refactors
posts:publish to delegate to WordPressPublisher without changing its
CLI output contract (existing tests pass unchanged).

Also fixes two bugs found along the way: UpdatePostRequest was silently
dropping `published_url` from PATCH requests (never in its validation
rules, despite AI_API.md documenting it), and three Livewire index-page
tests were broken by the earlier auth change (never re-run after adding
the `auth` middleware) since they hit routes via HTTP without a logged-in
user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
m105
2026-07-21 14:04:45 +03:00
co-authored by Claude Sonnet 5
parent 0cf72b44b7
commit 9278d2e8a0
26 changed files with 735 additions and 76 deletions
+24 -2
View File
@@ -1,9 +1,13 @@
<div>
<div @if ($hasPublishing) wire:poll.3s @endif>
<div class="flex justify-between items-center mb-4">
<h1 class="text-xl font-semibold">Posts</h1>
<button wire:click="create" class="bg-blue-600 text-white px-4 py-2 rounded">Add Post</button>
</div>
@if ($publishNotice)
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 text-amber-800 rounded text-sm">{{ $publishNotice }}</div>
@endif
<table class="w-full bg-white border border-gray-200 rounded">
<thead>
<tr class="text-left border-b border-gray-200">
@@ -22,10 +26,22 @@
<td class="p-3">{{ $post->site->name }}</td>
<td class="p-3">{{ ucfirst($post->type) }}</td>
<td class="p-3">{{ $post->title ?: \Illuminate\Support\Str::limit($post->body, 40) }}</td>
<td class="p-3">{{ ucfirst($post->status) }}</td>
<td class="p-3">
@if ($post->status === 'published' && $post->published_url)
<a href="{{ $post->published_url }}" target="_blank" class="text-green-700 hover:underline">Published</a>
@else
{{ ucfirst($post->status) }}
@endif
@if ($post->status === 'failed' && $post->publish_error)
<div class="text-xs text-red-600 mt-0.5">{{ $post->publish_error }}</div>
@endif
</td>
<td class="p-3">{{ $post->scheduled_at?->format('Y-m-d H:i') ?? '—' }}</td>
<td class="p-3">{{ $post->socialAccount?->username ?? '—' }}</td>
<td class="p-3 text-right space-x-2">
@if (in_array($post->type, ['blog', 'reddit']) && in_array($post->status, ['draft', 'scheduled', 'failed']))
<button wire:click="publish({{ $post->id }})" class="text-green-700">Publish</button>
@endif
<button wire:click="edit({{ $post->id }})" class="text-blue-600">Edit</button>
<button wire:click="delete({{ $post->id }})" wire:confirm="Delete this post?" class="text-red-600">Delete</button>
</td>
@@ -84,6 +100,12 @@
@error('body') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium">Target (optional — e.g. subreddit for Reddit posts)</label>
<input type="text" wire:model="target" class="w-full border rounded p-2">
@error('target') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium">Status</label>
<select wire:model="status" class="w-full border rounded p-2">