From 6c135ca56ff3d6639dc4a8d8b09f710fd882db74 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sat, 18 Jul 2026 16:43:09 +0300 Subject: [PATCH] docs: add design spec for Scheduled Posts feature Co-Authored-By: Claude Sonnet 5 --- .../specs/2026-07-18-posts-design.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-18-posts-design.md diff --git a/docs/superpowers/specs/2026-07-18-posts-design.md b/docs/superpowers/specs/2026-07-18-posts-design.md new file mode 100644 index 0000000..36fb4e8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-posts-design.md @@ -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.