docs: add design spec for Scheduled Posts feature
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
743bba3a65
commit
6c135ca56f
@@ -0,0 +1,81 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user