# Scheduled Posts — Design ## Purpose Extends the existing Sites & Social Accounts Manager with a **Posts** resource: a content calendar that an AI agent can use to draft content, schedule it against a specific site (and optionally a specific social account), and track its publishing lifecycle. This app is the data store and scheduling calendar only — it does not publish anything itself. The AI is responsible for reading due posts, publishing them externally (to Twitter, a blog, etc.), and reporting the result back via the API. ## Scope - CRUD UI for Posts (Livewire, consistent with the existing Sites/Social Accounts pages) - Full read/write JSON API for Posts (unlike the read-only Sites/Accounts API, since the AI drives the whole post lifecycle: draft → schedule → publish/fail) - A post belongs to exactly one Site (required) and optionally one Social Account - No authentication anywhere (UI or API) — local network only, consistent with the rest of the app - No background scheduler or per-platform publishing integration — publishing is entirely the AI's responsibility; this app only tracks what's due and what happened Out of scope: - Automatic/scheduled publishing (no queue job that calls out to Twitter/blog APIs) - Enforced consistency between a post's `type` and its linked account's `network` — these are independent fields by design - Edit history / versioning of post content - Authentication/authorization (UI or API) ## Data Model ### `posts` table | Column | Type | Constraints | |---|---|---| | id | bigint | PK | | site_id | bigint | required, FK → `sites.id` | | social_account_id | bigint | nullable, FK → `social_accounts.id` | | type | string (enum) | required — one of: `twitter`, `instagram`, `facebook`, `reddit`, `tiktok`, `linkedin`, `youtube`, `blog`, `other` (same network vocabulary as `SocialAccount::NETWORKS`, plus `blog`) | | title | string | nullable | | body | text | required | | status | string (enum) | required, default `draft` — one of: `draft`, `scheduled`, `published`, `failed` | | scheduled_at | timestamp | nullable | | created_at / updated_at | timestamps | | Relationships: - `Post belongsTo Site` (required) - `Post belongsTo SocialAccount` (nullable) - No reverse enforcement — a Site or SocialAccount can be deleted independent of posts referencing it unless a DB-level constraint is added (see Testing/Edge Cases below for the chosen behavior). No validation ties `type` to the linked account's `network` — a post can be `type=twitter` with no account linked, or linked to an Instagram account with `type=blog`, etc. This is intentional. ## UI (Livewire, no auth) New top-level nav item **"Posts"**, alongside the existing Sites and Social Accounts links. - **Table view**: site name, type, title (or a truncated body preview if no title), status, scheduled_at, linked account (or "—"), actions (edit/delete) - **Create/Edit modal form**: - Site — dropdown (required), populated from all Sites - Social Account — dropdown (optional, "None" default), populated from all Social Accounts - Type — dropdown (required): twitter, instagram, facebook, reddit, tiktok, linkedin, youtube, blog, other - Title — text input (optional) - Body — textarea (required) - Status — dropdown (required): draft, scheduled, published, failed (default draft) - Scheduled At — datetime input (optional) - Delete with confirmation ## API (full CRUD, no auth, JSON) Standard Laravel API resource controller with all five REST actions (this resource is read+write, unlike Sites/Accounts). - `GET /api/posts` — list all posts - `?status=` — optional exact match filter - `?site_id=` — optional exact match filter - `?social_account_id=` — optional exact match filter - `?due=1` — optional: filters to posts where `status=scheduled` AND `scheduled_at <= now()` — the "what needs publishing right now" query the AI polls - `GET /api/posts/{id}` — single post - `POST /api/posts` — create a post (site_id and body required at minimum; type required; other fields optional per the table above) - `PUT /api/posts/{id}` / `PATCH /api/posts/{id}` — update a post (e.g. AI sets status to `published` or `failed` after attempting to publish externally, or edits content/schedule) - `DELETE /api/posts/{id}` — delete a post JSON response shape: `{id, site_id, social_account_id, type, title, body, status, scheduled_at, created_at, updated_at}`. Validation for create/update: `site_id` required and must reference an existing site; `social_account_id` nullable, must reference an existing account when present; `type` required, must be one of the enum values; `body` required; `title` nullable; `status` required, must be one of the enum values, defaults to `draft` if omitted on create; `scheduled_at` nullable, valid datetime when present. ## Testing / Edge Cases - Feature tests for CRUD flows (Livewire component tests), mirroring the existing Sites/SocialAccounts test structure - Feature tests for the API: list, filter (status/site_id/social_account_id/due), show, create, update (including status transitions), delete, validation failures, 404s - Deleting a Site or Social Account that has posts referencing it: use `nullOnDelete()` for `social_account_id` (post keeps existing, just loses its account link) and `cascadeOnDelete()` for `site_id` (a post cannot exist without its site, so deleting the site deletes its posts) — this keeps the data model consistent without requiring UI-level guardrails against deletion.