Create content-hub-phase5-architecture.md via n8n

This commit is contained in:
herbygitea 2026-03-24 20:38:40 +00:00
parent 66a6b55f20
commit 112c23d44f

View File

@ -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 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:**
```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