Commit Graph
56 Commits
Author SHA1 Message Date
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 240fd3c3f6 Trust reverse proxy headers to fix mixed-content on the subdomain
App is served behind a reverse proxy that terminates TLS and forwards
plain HTTP to nginx on :8001. Without TrustProxies, Laravel couldn't
see the original request was HTTPS, so Livewire generated an http://
update endpoint on an https:// page — browsers block that as mixed
active content. Trusting proxy headers fixes URL generation while
leaving direct LAN http:// access on :8001 unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 19:23:23 +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
m105andClaude Sonnet 5 745b6965f3 Replace default Laravel README with project-specific docs
Document what Muninn does (content-operations hub for Sites, Social
Accounts, and Posts with a JSON API for AI agents) and how to set it
up locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 16:17:55 +03:00
Doar Mihai 6ca3750afb pythons cripts 2026-07-19 17:52:14 +03:00
Doar Mihai 4bd21f3234 ai docs 2026-07-19 17:23:06 +03:00
Doar Mihai 6bfa5448c7 Merge branch 'worktree-wordpress-publish' 2026-07-19 17:11:32 +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 Mihai 88c8203931 docs: add implementation plan for WordPress publishing command 2026-07-19 16:40:33 +03:00
Doar Mihai 88aebe298c docs: add design spec for WordPress publishing command
Extends Sites with cms_type + WP Application Password credentials
and adds a posts:publish Artisan command to push a Post to a
WordPress site's REST API, mirroring the reddit-post sidecar's
role for a native, non-browser-automation CMS target.
2026-07-19 16:40:33 +03:00
Doar Mihai a5fca5f0a9 docs: add implementation plan for WordPress publishing command 2026-07-19 16:24:23 +03:00
Doar Mihai deb8bcce8b docs: add design spec for WordPress publishing command
Extends Sites with cms_type + WP Application Password credentials
and adds a posts:publish Artisan command to push a Post to a
WordPress site's REST API, mirroring the reddit-post sidecar's
role for a native, non-browser-automation CMS target.
2026-07-19 16:21:46 +03:00
Doar MihaiandClaude Sonnet 5 0154c058af polish: trim unknown-arg echo, document trace artifacts and unverified selectors
Addresses whole-branch review's minor findings:
- Unknown CLI arguments now echo only the flag name, not its value, so a
  mistyped flag (e.g. --pasword=secret) can't leak a credential into the
  error output.
- README now notes that failed runs retain a screenshot/trace under
  test-results/ (gitignored, may contain the username but not the masked
  password), and points at the UNVERIFIED post-composer selectors as the
  first thing to check during real home-network verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 19:03:50 +03:00
Doar MihaiandClaude Sonnet 5 a95440751f feat: add CLI wrapper script for the reddit-post sidecar
Implements run.sh with argument parsing (--username, --password, --subreddit,
--title, --body, optional --headed), environment variable mapping to Task 2's
env contract, Playwright invocation with proper exit-code capture via set +e,
and result file output to stdout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 18:42:08 +03:00
Doar MihaiandClaude Sonnet 5 f794a5ae83 docs: annotate verified vs unverified selectors, drop redundant clicks
Addresses reviewer's minor findings on the prior fix commit: makes the
screenshot-verified/unverified boundary explicit in code comments so it
travels with the file rather than living only in commit messages, and
removes redundant .click() calls before .fill() (fill() already focuses).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 18:40:54 +03:00
Doar MihaiandClaude Sonnet 5 3edbf3fb26 fix: correct Reddit login selectors and detect network-level blocks
Live verification against a real Reddit login page (from this dev
sandbox) showed:
- The actual submit button has no [type="submit"] attribute; its
  accessible name is "Log In" — switched to getByRole with that name.
- Username/password fields are better targeted via their visible
  labels (getByLabel) than brittle name= attribute selectors.
- A cookie-consent dialog can overlay the form and intercept clicks;
  it's now dismissed via "Reject Optional Cookies" before filling in.
- This sandbox's network is flagged by Reddit's bot detection with a
  "blocked by network security" interstitial before the login form
  even loads — this is unrelated to credentials and unrelated to the
  target account (confirmed reachable fine from a residential
  network/browser). Added explicit detection for this state so it
  reports a clear, distinguishable error instead of a generic
  CAPTCHA/2FA guess. Full live end-to-end verification (successful
  login + post creation) must be run from a network Reddit does not
  block, e.g. the mini PC this app actually runs on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 18:38:04 +03:00
Doar Mihai a6fb5ef1cf fix: preserve specific error message on unexpected test exceptions
The outer catch's overwrite guard checked `!outcome.error`, but outcome.error
is always truthy (even the default placeholder string), so an exception
occurring outside the two explicitly-guarded blocks (e.g. a .fill()/.click()
timeout) would leave the generic "Test did not complete" message instead of
the real error. Replaced with an explicit outcomeFinalized sentinel that is
set exactly where outcome is deliberately finalized (missing env var, login
timeout, post-submission timeout, success), so any other exception correctly
falls through to the catch-all branch with its real message.
2026-07-18 18:15:41 +03:00
Doar Mihai 6982aa56bf feat: add Reddit login and post-creation Playwright test 2026-07-18 18:14:29 +03:00
Doar Mihai 2e53a8767b chore: scaffold reddit-post Playwright sidecar project 2026-07-18 18:08:18 +03:00
Doar MihaiandClaude Sonnet 5 b237617c35 docs: add implementation plan for Reddit post Playwright sidecar
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 18:02:49 +03:00
Doar MihaiandClaude Sonnet 5 44040e7c1e docs: add design spec for Reddit post Playwright sidecar
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 18:00:20 +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 4293475ce1 docs: add implementation plan for Scheduled Posts feature
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 16:53:18 +03:00
Doar MihaiandClaude Sonnet 5 6c135ca56f docs: add design spec for Scheduled Posts feature
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 16:43:09 +03:00
Doar Mihai 743bba3a65 feat: add home page linking to Sites and Social Accounts 2026-07-18 16:09:37 +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 MihaiandClaude Sonnet 5 3f46eecff8 chore: commit package-lock.json for reproducible npm installs
Generated during Task 5 (first task requiring npm install for the
Vite manifest); committing it so future installs resolve identical
dependency versions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 15:01:32 +03:00
Doar Mihai e75a846a97 feat: add Sites Livewire CRUD page 2026-07-18 14:57:47 +03:00