pbs-obsidian-vault/PBS/Tech/Projects/content-hub-phase5-architecture.md

12 KiB
Raw Permalink Blame History

project type status tags created updated path
content-hub-phase5-architecture project-plan active
pbs
flask
mysql
n8n
instagram
automation
docker
traefik
2026-03-19 2026-03-19 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 14 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:

# 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