34 Commits
Author SHA1 Message Date
m105andClaude Sonnet 5 6a51fda61e Add thumbnail, metadata, and YouTube publishing handlers (#40, #41, #42)
ThumbnailGenerationHandler (#40): generates thumbnail candidates via
StabilityAI, one per style (default: two contrasting styles) — all
stored as assets on the same step, so the existing approval UI's
"show every asset on this step side by side" behavior is the A/B
comparison the task calls for, no extra UI needed.

MetadataGenerationHandler (#41): SEO title/description/tags/hashtags
from the project's script via OpenAI in JSON mode, same pattern as
StoryboardHandler.

YoutubePublishHandler (#42): real OAuth2 + YouTube Data API v3
resumable upload (token refresh -> init session -> PUT video bytes),
supporting scheduling via publishAt (forces privacyStatus=private per
YouTube's own constraint). Reuses the AiProvider table for credentials
(provider='youtube', api_key=refresh_token, config=client_id/secret)
rather than a new table — added 'youtube' to AiProvider::PROVIDERS.
Does not cover analytics retrieval: that's an ongoing query against a
published video, not a one-shot publish action, so it doesn't fit the
per-video step model at all.

All 8 WorkflowStep::TYPES now have real handlers, so the "unhandled
type" test fixtures (which used real-but-not-yet-implemented types)
would go stale every time a new module lands — switched them to
fabricated type strings so they stay meaningful regardless of what
gets built next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 21:26:56 +03:00
m105andClaude Sonnet 5 c249a14d78 Add storyboard/video-gen/video-editing handlers and asset previews (#37-39, #45, #46)
StoryboardHandler (#37): breaks the project's script into a numbered
scene-by-scene breakdown (description + image prompt per scene) via
OpenAI in JSON mode (added json_mode support to OpenAiDriver for
reliable structured output), stored as a JSON asset.

VideoGenerationHandler (#38): generates one still image per storyboard
scene via StabilityAI — the scene *visuals*, not motion/video clips
(no text-to-video model integrated; see the task comment for why).

VideoEditingHandler (#39): the step that actually produces a video
file — assembles the scene stills into an MP4 via ffmpeg's concat
demuxer, timed to the voiceover audio's duration (probed via ffprobe)
or a fixed per-scene duration if there's no audio, then muxes the
audio track in. Verified against the real ffmpeg/ffprobe binaries on
this box, not just Process::fake().

Asset previews (#45, #46): a reusable partial renders each asset
inline by type — image/audio/video get real playback elements, text/
JSON gets a truncated <pre> block, everything else a download link.
Wired into both the awaiting_approval step view (so approving/
rejecting is an actual content review, not just a word count) and a
general project assets grid.

Also fixes a latent bug in AssetStorage::url(), found while wiring up
previews: it called Storage::url() on the default 'local' disk, which
has no configured URL driver and throws in production — only worked
before because Storage::fake() is more permissive than the real local
driver. Now serves assets through a new authenticated streaming route
instead (VideoProjectAssetController).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 21:15:31 +03:00
m105andClaude Sonnet 5 55fa2ebfda Add script/TTS step handlers, dashboard, and pipeline UI (#35, #36, #43, #44)
StepHandler abstraction (app/Support/VideoWorkflow/StepHandlers/) plugs
into WorkflowEngine without coupling it to how any given step type is
actually executed:
- ScriptGenerationHandler: builds a prompt from the project's title/
  description (or an explicit outline/tone/keywords/target length in
  step input), generates via an active OpenAI AiProvider, stores the
  result as a 'script' asset.
- TtsVoiceoverHandler: voices step.input.text, or falls back to the
  project's latest script asset, via an active ElevenLabs AiProvider;
  voice_id/model_id are configurable per step. No direct "speed"
  control — ElevenLabs' base TTS endpoint doesn't expose one.

RunWorkflowStepJob resolves a step's handler by type and runs it on
the queue, matching the existing PublishPostJob pattern (including a
stale-dispatch guard: no-op if the step isn't 'in_progress' anymore).

UI, under its own /video-projects routes and nav entry — separate
from /projects per the earlier decision:
- VideoProjects/Index (#43): list projects with status and step
  progress, create new ones.
- VideoProjects/Show (#44): start a project from a WorkflowTemplate or
  a hardcoded default 8-step pipeline; per-step Run/Approve/Reject/
  Retry/Skip actions; auto-queues the next step after any transition
  when a handler exists for its type (steps with no handler yet just
  sit in_progress rather than auto-failing); lists project assets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 20:56:01 +03:00
m105andClaude Sonnet 5 0e315ada04 Add AI provider abstraction and asset storage (tasks #33, #34)
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>
2026-08-06 20:47:06 +03:00
m105andClaude Sonnet 5 a4ff6c0ccb Commit the Projects/Tasks/Ideas/TaskComments feature
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>
2026-08-06 20:41:57 +03:00
m105andClaude Sonnet 5 6ff7f78c9c Add Video Projects schema and workflow engine core (tasks #31, #32)
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>
2026-08-06 20:38:52 +03:00
m105 efdec9eabd Add optional Tone field to Sites for AI content generation
- 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>
2026-07-28 22:06:17 +03:00
m105 cee9fdc290 Add Webhook Endpoints for external automation
- 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>
2026-07-27 21:50:17 +03:00
m105 f3c7f1d9ca Add Advanced Search and filtering to Posts
- 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>
2026-07-27 21:45:17 +03:00
m105 90f313c134 Add Cross-Posting support for multiple social accounts
- 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>
2026-07-27 21:24:49 +03:00
m105 44f979820f Add Twitter and LinkedIn publishers with Playwright sidecars
- 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>
2026-07-27 21:08:39 +03:00
m105 2a41328130 Add Post Templates feature
- 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>
2026-07-27 21:00:41 +03:00
m105andClaude Sonnet 5 b3b250aaca Add free-text api_instructions field to Sites
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>
2026-07-22 14:03:43 +03:00
m105andClaude Sonnet 5 4b3af4cc89 Make the image optional for WordPress posts too
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>
2026-07-21 20:07:47 +03:00
m105andClaude Sonnet 5 42abc658f2 Add an image field, required for WordPress posts
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>
2026-07-21 20:03:25 +03:00
m105andClaude Sonnet 5 fd185ab813 Guard PublishPostJob against duplicate/stale dispatches
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>
2026-07-21 19:29:44 +03:00
m105andClaude Sonnet 5 9278d2e8a0 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>
2026-07-21 14:04:45 +03:00
m105andClaude Sonnet 5 0cf72b44b7 Require auth for the web UI, keep the JSON API open
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>
2026-07-21 12:46:50 +03:00
Doar Mihai f7b71684f1 test: cover credential-clearing and trailing-slash URL edge cases
Also fixes a missing trailing newline in SiteResource.php.
2026-07-19 17:05:01 +03:00
Doar Mihai c2ef6d9986 feat: manage cms_type and WordPress credentials in Sites UI 2026-07-19 16:58:42 +03:00
Doar Mihai 336436bdc7 feat: add posts:publish command for WordPress REST API publishing 2026-07-19 16:54:38 +03:00
Doar Mihai 5501b93686 feat: expose cms_type/WordPress credentials and published_url in API responses 2026-07-19 16:51:15 +03:00
Doar MihaiandClaude Sonnet 5 80423301c4 feat: add published_url column to posts
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:48:03 +03:00
Doar Mihai 30095519e4 feat: add cms_type and WordPress credential fields to sites 2026-07-19 16:43:13 +03:00
Doar MihaiandClaude Sonnet 5 e0b8f2f924 feat: add full CRUD Posts API
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>
2026-07-18 17:22:16 +03:00
Doar MihaiandClaude Sonnet 5 be8e6e997f feat: add Posts Livewire CRUD page
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>
2026-07-18 17:13:50 +03:00
Doar MihaiandClaude Sonnet 5 f746c5d6a0 feat: add Post model, migration, and factory
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>
2026-07-18 17:07:48 +03:00
Doar MihaiandClaude Sonnet 5 5c24b6fbe0 feat: add read-only Social Accounts API
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>
2026-07-18 15:25:56 +03:00
Doar MihaiandClaude Sonnet 5 20c33f1c23 feat: add read-only Sites API
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>
2026-07-18 15:12:44 +03:00
Doar MihaiandClaude Sonnet 5 cee5b1976b feat: add Social Accounts Livewire CRUD page
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 15:05:55 +03:00
Doar Mihai e75a846a97 feat: add Sites Livewire CRUD page 2026-07-18 14:57:47 +03:00
Doar MihaiandClaude Sonnet 5 137a15f5ff feat: add SocialAccount model, migration, and factory
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>
2026-07-18 14:50:00 +03:00
Doar MihaiandClaude Sonnet 5 889ac9b7f9 feat: add Site model, migration, and factory
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>
2026-07-18 14:45:55 +03:00
Doar MihaiandClaude Sonnet 5 900e4f0309 Initial commit: Laravel skeleton with sites & accounts spec/plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 14:35:53 +03:00