diff --git a/PBS/Tech/Projects/content-hub-phase5-architecture.md b/PBS/Tech/Projects/content-hub-phase5-architecture.md new file mode 100644 index 0000000..a7d92c3 --- /dev/null +++ b/PBS/Tech/Projects/content-hub-phase5-architecture.md @@ -0,0 +1,363 @@ +--- +project: content-hub-phase5-architecture +type: project-plan +status: active +tags: + - pbs + - flask + - mysql + - n8n + - instagram + - automation + - docker + - traefik +created: 2026-03-19 +updated: 2026-03-19 +path: PBS/Tech/Projects/ +--- + +# PBS Content Hub Phase 5 — Architecture & Planning Decisions + +## Purpose + +This document captures all architecture and planning decisions made during +the Phase 5 planning session. It serves as a handoff doc for Claude Code to +implement the Content Hub refactor. + +--- + +## Context + +### Phases 1–4 Complete +- ✅ Phase 1 — MySQL schema with single `instagram_posts` table +- ✅ Phase 2 — Reply workflow refactored with error notifications +- ✅ Phase 3 — WordPress → MySQL sync via webhook +- ✅ Phase 4 — Instagram reel publish webhook capturing reel IDs + +### Two Existing Apps Being Merged + +**PBS-API (production, running on Linode):** +- Location: `/opt/docker/pbs-api/app.py` +- Container: `pbs-api` on internal Docker network +- Internal URL: `http://pbs-api:5000` +- External URL: `https://plantbasedsoutherner.com/api/...` (via Traefik) +- Database: MySQL `pbs_automation`, connects as user `pbs_api` +- Auth: API key via `X-API-Key` header on all endpoints (except +`/api/health`), Traefik BasicAuth on `/pbsadmin` routes +- Current endpoints: + - `GET /api/health` — health check (no auth) + - `POST /api/instagram-mapping` — save/update recipe mappings (upsert) + - `GET /api/instagram-mapping/{keyword}` — lookup recipe by keyword + - `GET /api/recipes/all` — bulk fetch all recipes + - `GET /pbsadmin/instagram/recipes` — Jenny's admin interface (inline +keyword editing, search/filter, stats) +- MySQL tables (`pbs_automation`): + - `instagram_recipes` — post_id (PK), recipe_title, recipe_url, keyword, +created_at, updated_at + - `instagram_posts_processed` — instagram_post_id (PK), last_checked, +comment_count + - `instagram_replies` — id (auto PK), instagram_comment_id (unique), +instagram_post_id, comment_text, reply_text, recipe_url, sent_at +- Security: API key stored in `.env` file, Traefik BasicAuth for +`/pbsadmin` with username `jenny` + +**PBS Content Hub (local development, Travis's machine):** +- CLI + Flask GUI using SQLite/SQLAlchemy +- CLI commands: `pbs init`, `pbs convert`, `pbs scan`, `pbs archive`, `pbs +status`, `pbs list`, `pbs shell`, `pbs activate/deactivate` +- Flask GUI: Dashboard, Project Detail (Overview, Checklist, Description +Builder), Library CRUD +- Models: `Project`, `VideoFile`, `ChecklistItem`, `LibraryItem` +- Library uses single `content_blocks` table: `id, name, content, +block_type, sort_order` + +--- + +## Architecture Decisions + +### Decision 1: Single Container (pbs-hub) + +**Decision:** Merge PBS-API and Content Hub into ONE Flask app in ONE +container. + +**Rationale:** +- Two containers is over-engineered for current scale (team of 2 + +assistant, one automation consumer) +- The admin page Jenny uses today belongs in the same UI as the new Content +Hub features +- Avoids maintaining two Flask apps, two Dockerfiles, two deployments +- The code cleanliness concern is solved with Flask Blueprints, not +separate containers +- If a two-container split is ever needed (membership site, public API), +the service layer architecture makes extraction straightforward later + +### Decision 2: Flask Blueprints for Code Organization + +**Decision:** Use Flask Blueprints to separate API routes from web UI +routes. + +**Target project structure:** +``` +pbs-hub/ +├── app.py (creates Flask app, registers blueprints) +├── blueprints/ +│ ├── api/ (all JSON endpoints — n8n, CLI, auto-match) +│ │ ├── routes.py +│ │ └── ... +│ ├── web/ (all HTML pages — Jenny's UI, dashboard, reels) +│ │ ├── routes.py +│ │ └── templates/ +│ └── ... +├── services/ (business logic — match routine, recipe CRUD) +├── models/ (SQLAlchemy models) +└── ... +``` + +### Decision 3: Service Layer Refactor + +**Decision:** Extract all SQLAlchemy logic into a service layer. + +- Flask routes (both API and web) call service functions, never query the +DB directly +- CLI will call the API endpoints over HTTPS (not import service functions) +- This is the key enabler for the CLI → API migration + +**Example pattern:** +```python +# services/project_service.py +def get_all_projects(): + return Project.query.all() + +def create_project(title): + project = Project(title=title) + db.session.add(project) + db.session.commit() + return project + +# blueprints/api/routes.py +@api_bp.route('/api/projects', methods=['GET']) +def api_list_projects(): + projects = get_all_projects() + return jsonify([p.to_dict() for p in projects]) + +# blueprints/web/routes.py +@web_bp.route('/projects') +def list_projects(): + projects = get_all_projects() + return render_template('projects.html', projects=projects) +``` + +### Decision 4: SQLite → MySQL Migration + +**Decision:** Replace SQLite with MySQL. SQLAlchemy abstracts the engine, +so models barely change — mostly a connection string swap + migration. + +- Content Hub connects to the same MySQL instance as the current PBS-API +- Database: `pbs_automation` (existing) or new `pbs_content_hub` database +(TBD) +- MySQL user: `pbs_api` (existing, has access to `pbs_automation.*`) + +### Decision 5: CLI Talks to API Over HTTPS + +**Decision:** The pbsVM CLI will make HTTPS calls to the Content Hub API +instead of direct SQLAlchemy queries. + +- Auth: API key in a local config file or environment variable +- Uses `httpx` (or `requests`) to call API endpoints +- No database credentials on Travis's local machine +- CLI needs network access to push data (file management operations remain +local) + +--- + +## UI & Feature Decisions + +### Decision 6: Recipe-to-Reel Linking Strategy + +**Decision:** Manual selection in Content Hub. + +Jenny selects the recipe from a dropdown when creating a reel record. This +auto-populates the post ID, keyword, and URL. No caption parsing, no +fragile keyword matching. + +**Current process being replaced:** Jenny/assistant emails Travis for the +post ID → Travis runs MySQL query → sends back ID. The Content Hub removes +Travis from this loop entirely. + +### Decision 7: Reel Record Lifecycle (Two-Stage) + +**Decision:** Local-first, then matched to live data. + +- **Stage 1 (Pre-publish):** Jenny creates a reel record in the Content Hub +before anything exists on Instagram. Working title, linked recipe, keyword, +built caption. No Instagram reel ID yet. +- **Stage 2 (Post-publish):** Instagram webhook fires → n8n writes reel ID +to `instagram_posts` → hub reads that table and matches to local record. + +### Decision 8: n8n Stays Decoupled + +**Decision:** n8n is dumb to the hub. + +- n8n writes to `instagram_posts` in MySQL — that's its only job +- n8n does NOT call Content Hub API or know about hub reel records +- The Content Hub reads from `instagram_posts` to display what's live +- The hub owns all matching logic +- **One exception:** n8n sends a simple ping to `POST /api/match/run` after +new inserts to trigger the auto-match routine + +### Decision 9: Auto-Match Routine + +**Decision:** Triggered by n8n ping after new `instagram_posts` insert. + +**Logic:** +``` +For each hub reel record in "Ready" status (no reel ID linked): + → Look in instagram_posts for unmatched rows where: + - post_id matches (recipe link) OR keyword matches + → Single confident match → auto-link, status → "Live — Matched" + → Multiple matches → flag as "Needs Review" + → No match → leave as-is +``` + +Manual match UI is the fallback for anything auto-match can't resolve. + +### Decision 10: Recipe Dropdown Source + +**Decision:** MySQL recipes table (not WordPress REST API). Already synced +via webhook, faster, no external calls during form interactions. + +### Decision 11: Reel Detail Page — Tabbed Layout + +``` +[ Overview ] [ Caption Builder ] +``` + +**Overview tab:** +- Reel Title (free text) +- Recipe dropdown (from MySQL) → auto-fills keyword + URL +- Keyword (editable — this IS the recipe review/confirm step) +- Recipe URL (read-only) +- Status indicator +- Save / Delete + +**Caption Builder tab (separate tab within reel detail):** +- Select library blocks (hashtags, links, CTAs, about snippets) +- Preview assembled caption +- Copy to clipboard + +### Decision 12: Recipe Review UI + +**Decision:** Not a separate screen. Baked into the reel creation flow — +when Jenny selects a recipe from the dropdown, she sees the record and +confirms/edits the keyword right there. + +### Decision 13: Manual Match Correction + +A "Link Reel" button on flagged records. Jenny selects from unmatched +`instagram_posts` rows to connect the dots. + +--- + +## Reel Status States + +1. **Draft** — reel record created, no recipe linked yet +2. **Ready** — recipe linked, keyword confirmed, caption built. Waiting to +be posted +3. **Live — Matched** — Instagram webhook received, reel ID attached, +auto-reply active +4. **Live — Needs Review** — auto-match couldn't confidently link, Jenny +needs to manually match + +--- + +## Reel List View + +Data points per reel (layout TBD — table vs cards, getting Jenny's +feedback): +- Reel title +- Linked recipe name (or "No Recipe" warning) +- Keyword +- Status (color dot + label) +- Date created or last updated + +--- + +## Content Hub Navigation (from earlier design doc) + +``` +[ Dashboard ] [ Projects ] [ Reels ] [ Library ] [ Status ] [ Settings +] +``` + +| Tab | Primary User | Description | +|-----|-------------|-------------| +| Dashboard | Both | Overview of recent projects and reels | +| Projects | Travis | Video project management (existing pbsVM GUI) | +| Reels | Jenny | Reel manager, caption builder, publishing workflow | +| Library | Both | Shared hashtags, links, about blocks, CTAs | +| Status | Both | Automation health, comment/reply counts, healthcheck | +| Settings | Travis | Global config, API keys, default checklist | + +--- + +## Target Automation Flow + +``` +Jenny creates reel record in hub → selects recipe → builds caption + ↓ +Jenny posts reel to Instagram + ↓ +Instagram webhook → n8n → writes reel ID to instagram_posts + ↓ +n8n pings hub (POST /api/match/run) + ↓ +Hub auto-match runs: + - Finds match → links record → status = Live — Matched ✅ + - No confident match → status = Needs Review 🟡 + ↓ +Comment comes in on Instagram → n8n looks up instagram_posts → sends DM + +reply + ↓ +Dashboard shows green across the board 🟢 +``` + +--- + +## Refactor Steps (Implementation Order) + +1. **Extract service layer** from current Content Hub SQLAlchemy code +2. **Restructure into Blueprints** (api/ and web/) +3. **Add API routes** alongside existing GUI routes +4. **Migrate PBS-API endpoints** into the new Blueprint structure +5. **Migrate Jenny's admin page** into the Content Hub web UI +6. **Swap SQLite → MySQL** (connection string + migration) +7. **Refactor CLI** to use `httpx` calls to API instead of direct SQLAlchemy +8. **Build Reels tab** (creation form, recipe dropdown, status lifecycle) +9. **Build Caption Builder tab** within reel detail +10. **Build auto-match routine** (`POST /api/match/run`) +11. **Build manual match UI** (fallback for auto-match failures) +12. **Build Status Dashboard** (per-reel health + system health — details +TBD) +13. **Deploy to staging** → test → deploy to production +14. **Update n8n** to point at new container URL + add match/run ping +15. **Retire PBS-API container** + +--- + +## Open Questions (Next Planning Session) + +- [ ] Reel list layout: table rows vs cards (get Jenny's feedback) +- [ ] Jenny's assistant access: same login or separate user accounts? +- [ ] Status Dashboard UI details: per-reel health indicators + system +health panel +- [ ] Database schema for hub reel records table +- [ ] Define green/yellow/red status thresholds in detail +- [ ] Database strategy: extend `pbs_automation` or create new +`pbs_content_hub` database? + +--- + +*Project: Plant Based Southerner Content Hub* +*Planning Session: March 19, 2026* +*Participants: Travis & Claude* +...sent from Jenny & Travis \ No newline at end of file