AiProviderDriver: one generic interface (generate(prompt, options) ->
AiGenerationResult) rather than per-capability interfaces, since text
(OpenAI chat completions), image (StabilityAI stable-image), and
speech (ElevenLabs TTS) generation all reduce to prompt-in/content-out.
Drivers throw AiGenerationException on failure; AiProviderManager
resolves the right driver from an AiProvider record's `provider`
column and guards on is_active. Real request/response shapes per each
provider's API, verified with Http::fake() — no live API keys
available yet to smoke test against the real services.
AssetStorage: stores generated content under
video-projects/{id}/{type}/v{n}.{ext}, versioned per (project, type[,
step]) slot so retries don't clobber earlier output, plus
contents/url/delete and pruneOldVersions for cleanup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was already implemented, migrated, and running live, but had
never been committed. Bringing it into git: Project/Task/Idea/
TaskComment models, their JSON API (CRUD + status transitions,
task comments, idea<->project linking), Livewire UI pages, and the
nav/route wiring for them. All 42 of its own tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Foundational layer for the YouTube Content Creation System project,
built as a fully independent domain per the earlier decision (task
#47) — no foreign keys into the existing projects/tasks/ideas tables.
Schema: video_projects, workflow_steps, assets, ai_providers,
workflow_templates (named to avoid colliding with the existing
Post-templates `templates` table).
WorkflowEngine (app/Support/VideoWorkflow/) drives a project's step
sequence: start/startFromTemplate, currentStep, completeStep (with
optional approval gating), approveStep/rejectStep, failStep/retryStep,
skipStep. It only manages state transitions — actual step execution
(AI calls, rendering, etc.) is out of scope here (tasks #33+).
Also fixes four Publisher classes (WordPress/Reddit/Twitter/LinkedIn)
that were missing a `use App\Models\SocialAccount;` import after an
earlier interface change, which broke the entire test suite at
bootstrap; and PostFactory, which still set the now-removed
`social_account_id` column instead of the SocialAccount pivot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add tone column to sites table (nullable string)
- Add TONES constant to Site model with predefined options:
professional, casual, technical, friendly, formal, humorous,
conversational, authoritative, inspirational, educational, minimal, detailed
- Update Sites Livewire component to edit tone
- Update Sites view with tone select dropdown in form
- Add tone column to sites table display
- Add tone to SiteResource API response
AI agents can now read the site's tone and use it when generating content.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Create WebhookController with 4 endpoints:
- POST /api/webhooks/posts - Create a new post
- GET /api/webhooks/posts - List posts with filters
- POST /api/webhooks/posts/{post}/publish - Trigger publishing
- GET /api/webhooks/posts/{post}/status - Get post status
- Add X-Muninn-Token header authentication
- Add MUNINN_WEBHOOK_TOKEN config in services.php
- Add MUNINN_WEBHOOK_TOKEN to .env.example
- Add webhook routes with /webhooks prefix
All endpoints return JSON responses with success/error fields.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Add full-text search index on posts.title and posts.body
- Add search box with live filtering
- Add filter dropdowns for type, status, and site
- Add Clear Filters button
- Display post count in filter bar
- All filters work together (AND logic)
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Create post_social_account pivot table for many-to-many relationship
- Add migration to migrate existing social_account_id to pivot table
- Remove social_account_id column from posts table
- Update Post model with socialAccounts() BelongsToMany relationship
- Update Posts Livewire component to use array of selectedAccountIds
- Update Posts view with checkboxes for selecting multiple accounts
- Update PublishPostJob to iterate through all social accounts of matching type
- Update Publisher interface to accept optional SocialAccount parameter
- Update all Publishers (WordPress, Reddit, Twitter, LinkedIn) to support cross-posting
- Display multiple social accounts as badges in posts table
Now a single post can be published to multiple social accounts of the same type.
For example: a Twitter post can be published to multiple Twitter accounts.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Create twitter-post sidecar with Playwright for browser automation
- Create linkedin-post sidecar with Playwright for browser automation
- Add TwitterPublisher class that calls twitter-post sidecar
- Add LinkedInPublisher class that calls linkedin-post sidecar
- Register both publishers in PublisherManager via AppServiceProvider
- Update Posts index view to show Publish button for twitter/linkedin types
Both publishers use the same pattern as RedditPublisher:
- No API keys required (uses stored credentials from Social Accounts)
- Browser automation via Playwright
- Returns JSON result with success/failure and URL
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Create templates table with name, body, type, title, target, placeholders
- Add Template model with applyToPost method for placeholder replacement
- Add Livewire Templates/Index component for CRUD operations
- Add templates index view with form and table
- Add /templates route
- Update Posts/Index to support template selection when creating posts
- Templates can be applied to pre-fill post form fields
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
For custom sites we'll post to via their own API — there's no wired-in
publisher for these, so this is where the site owner documents how to
actually post there (auth, endpoints, payload shape). Shown in the UI
for non-WordPress sites, exposed via the JSON API, and pointed to from
AI_API.md's "any other network/CMS" fallback section so an agent checks
it before improvising.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Some blog posts just don't have one. WordPressPublisher no longer
rejects publishing a type=blog post without image_url — it uploads
and attaches it as the featured image when present, and simply skips
that step (no featured_media set) otherwise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Posts get a nullable image_url. WordPressPublisher now downloads it
and uploads it to WordPress's media endpoint before creating the
post, setting it as featured_media — and rejects publishing (without
touching status) if a blog post has no image set, mirroring the
existing title requirement. Every other network treats it as optional.
Not enforced at save time, same as title — you can still draft an
incomplete WordPress post, it just can't be published until an image
is set. Exposed through the UI form (with a thumbnail/""missing""
indicator in the table), the JSON API, and the Python CLI wrapper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Skip if the post is no longer status=publishing when the job runs, so
a stray duplicate (e.g. clicking Publish twice while no worker was
running) can't publish the same post to WordPress a second time.
Also root-caused a stuck-in-"publishing" post reported live: the
queue worker started manually last session was a bare background
process, not a real service, and had died. Replaced it with a
systemd unit (muninn-queue-worker.service, enabled + running) so
the Publish button keeps working across reboots/restarts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Sites/Social Accounts/Posts pages now require login; add a Login
Livewire component, guest layout, and logout action. routes/api.php
is untouched, per AI_API.md's documented no-auth-by-design contract.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds JSON API for Posts (index/show/store/update/destroy) so an AI
agent can draft posts, read what's due for publishing, and report
back publish/fail status. Unlike the existing read-only Sites/Accounts
APIs, this is full CRUD since the app itself never publishes anything.
- PostController with status/site_id/social_account_id/due filters
- StorePostRequest/UpdatePostRequest validate against Post::TYPES/STATUSES
- PostResource shapes the JSON response
- Route::apiResource('posts', ...) registered with ->names('api.posts')
to avoid colliding with the existing web posts.index route name
- store() calls ->fresh() after create() so the response reflects the
DB-level default status ('draft') instead of a stale in-memory null
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the Posts index page (list/create/edit/delete) following the
existing Sites/SocialAccounts pattern, and extends the shared nav
layout with a Posts link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the Post model belonging to Site (required, cascade-deletes with
the site) and SocialAccount (nullable, nulled out on account delete).
Foundation for the Scheduled Posts feature; later tasks build the
Livewire CRUD UI and API on top of this model.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add GET /api/accounts (list with optional ?network= and ?type= filters) and
GET /api/accounts/{socialAccount} (show) endpoints. Include plain-text password
field for AI agent consumption.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add GET /api/sites endpoint for listing sites with optional name filter,
and GET /api/sites/{site} for showing individual sites. Includes API
resource, controller, routes registration, and comprehensive tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implement SocialAccount model with enum fields for network and type,
along with migration and factory. Includes NETWORKS and TYPES constants
for dropdown usage in Livewire forms and validation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements Task 2: creates the Site model with fillable name, url, and
description fields; the corresponding database migration; and a factory
for generating test data. Includes TDD tests verifying model creation
with and without optional description.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>