--- 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