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
+7 -3
View File
@@ -5,10 +5,10 @@ Usage:
python3 posts.py list [--status STATUS] [--site-id ID] [--social-account-id ID] [--due]
python3 posts.py get POST_ID
python3 posts.py create --site-id ID --type TYPE --body BODY
[--social-account-id ID] [--title TITLE]
[--social-account-id ID] [--title TITLE] [--target TARGET]
[--status STATUS] [--scheduled-at DATETIME]
python3 posts.py update POST_ID [--site-id ID] [--social-account-id ID]
[--type TYPE] [--title TITLE] [--body BODY]
[--type TYPE] [--title TITLE] [--body BODY] [--target TARGET]
[--status STATUS] [--scheduled-at DATETIME]
[--published-url URL]
python3 posts.py delete POST_ID
@@ -22,7 +22,7 @@ import argparse
import _client
TYPES = ["twitter", "instagram", "facebook", "reddit", "tiktok", "linkedin", "youtube", "blog", "other"]
STATUSES = ["draft", "scheduled", "published", "failed"]
STATUSES = ["draft", "scheduled", "publishing", "published", "failed"]
def main() -> None:
@@ -46,6 +46,7 @@ def main() -> None:
create_parser.add_argument("--body", required=True)
create_parser.add_argument("--social-account-id", type=int)
create_parser.add_argument("--title")
create_parser.add_argument("--target", help="Network-specific destination, e.g. a subreddit for Reddit posts")
create_parser.add_argument("--status", choices=STATUSES)
create_parser.add_argument("--scheduled-at", help="ISO 8601 datetime, e.g. 2026-07-20T09:00:00")
@@ -56,6 +57,7 @@ def main() -> None:
update_parser.add_argument("--type", choices=TYPES)
update_parser.add_argument("--title")
update_parser.add_argument("--body")
update_parser.add_argument("--target", help="Network-specific destination, e.g. a subreddit for Reddit posts")
update_parser.add_argument("--status", choices=STATUSES)
update_parser.add_argument("--scheduled-at", help="ISO 8601 datetime")
update_parser.add_argument("--published-url")
@@ -85,6 +87,7 @@ def main() -> None:
"type": args.type,
"title": args.title,
"body": args.body,
"target": args.target,
"status": args.status,
"scheduled_at": args.scheduled_at,
}
@@ -98,6 +101,7 @@ def main() -> None:
"type": args.type,
"title": args.title,
"body": args.body,
"target": args.target,
"status": args.status,
"scheduled_at": args.scheduled_at,
"published_url": args.published_url,