Create content-hub-phase5-architecture.md via n8n
This commit is contained in:
parent
66a6b55f20
commit
112c23d44f
363
PBS/Tech/Projects/content-hub-phase5-architecture.md
Normal file
363
PBS/Tech/Projects/content-hub-phase5-architecture.md
Normal 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 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
|
||||
Loading…
Reference in New Issue
Block a user