diff --git a/PBS/Tech/Sessions/instagram-automation-session-notes.md b/PBS/Tech/Sessions/instagram-automation-session-notes.md new file mode 100644 index 0000000..c511221 --- /dev/null +++ b/PBS/Tech/Sessions/instagram-automation-session-notes.md @@ -0,0 +1,165 @@ +--- +project: instagram-automation-session-notes +type: session-notes +status: active +tags: + - pbs + - n8n + - instagram + - mysql + - wordpress + - automation + - docker +created: 2026-03-16 +updated: 2026-03-16 +path: PBS/Tech/Sessions/ +--- + +# Session Notes — MySQL Schema, Error Checking & The Great Lockout of 2026 + +## ✅ Accomplished This Session + +### Error Checking — Instagram Reply Workflow +All four error checks are now wired to Notify Travis → Google Chat: + +- ✅ Subscribe check notification +- ✅ Hash verification failure notification (with ⛔ alert level) +- ✅ Empty table lookup notification (includes Reel ID, commenter, comment text) +- ✅ Keyword not matched notification + +**Key learnings:** +- n8n "Always Output Data" needed on MySQL node so IF node can evaluate empty results +- When "Always Output Data" is on, check `$('node').first().json.id` exists instead of `length > 0` (length returns 1 for empty item) +- Google Chat HTTP Request node must use "Using Fields Below" mode, not raw JSON — literal newlines from expressions break JSON +- Build message strings as single `={{ }}` expression using `+` concatenation to keep `\n` as escaped characters +- `$now.toFormat('MMM dd, HH:mm:ss')` works in both test and live contexts (unlike `$execution.startedAt`) +- Timestamp and title passed from calling workflow, Notify Travis just passes them through + +--- + +### MySQL Schema — New Tables Created + +**`pbs_recipes`:** +```sql +CREATE TABLE IF NOT EXISTS pbs_recipes ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + post_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(500) NOT NULL, + url VARCHAR(500) NOT NULL, + keyword VARCHAR(100) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY (post_id) +); +``` + +**`instagram_posts`:** +```sql +CREATE TABLE IF NOT EXISTS instagram_posts ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + reel_id VARCHAR(50) NOT NULL, + post_id BIGINT UNSIGNED NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY (reel_id), + FOREIGN KEY (post_id) REFERENCES pbs_recipes(post_id) +); +``` + +**Key decisions:** +- `post_id` is nullable in `instagram_posts` — NULL means unmatched reel, not an error +- `reel_id` stored as VARCHAR not BIGINT (Instagram ID integer overflow issue) +- Normalized schema — keyword and URL live in `pbs_recipes` only, JOIN at query time +- Staying on shared MySQL instance — at current scale (2k/month, 10-20 replies) separate instance is overkill +- phpMyAdmin added back to Docker Compose for direct MySQL access + +--- + +### WordPress Publish Webhook — Proof of Concept Complete +- WPCode Lite snippet fires on post publish (admin only) +- Payload: post_id, title, url, tags, categories +- HMAC signature verification via X-PBS-Signature header +- Raw body passed via X-PBS-Body header (base64 encoded) for exact signature verification +- n8n Code node verifies signature before processing +- Ready to wire into `pbs_recipes` table insert workflow + +--- + +### Architecture Decision — Reply Workflow Refactor +Current two-table lookup + merge is being replaced with: +- Single `instagram_posts` lookup only +- URL stored directly in `instagram_posts` (populated at reel insert time from `pbs_recipes`) +- Simpler, faster, fewer failure points + +--- + +## 🔧 The Great WordPress Lockout of 2026 + +**What happened:** Wordfence locked out staging WP admin. Unlock email never arrived. + +**What we tried (none of it worked):** +- WP-CLI wf:unblock-ip command (not a registered command) +- Clearing wp_options Wordfence entries in MySQL +- Disabling Wordfence plugin +- Restarting WordPress container +- Deleting wflogs directory + +**Root cause discovered:** +Wordfence WAF runs as a PHP auto_prepend_file — completely independent of the plugin being active. Found in .htaccess and wordfence-waf.php in WordPress root. + +**Actual fix:** Wrong email address on the admin account. Changed it, logged right in. 🤦 + +**Real lesson learned:** Read the error message carefully before spending an hour troubleshooting! 😄 + +**Lockout duration if we hadn't figured it out:** 1 MONTH + +--- + +## 📋 Runbook Addition — WordPress Lockout Recovery + +```bash +# Get into WordPress container +docker exec -it wordpress bash +cd /var/www/html + +# List all users +wp --allow-root user list + +# Reset password by username +wp --allow-root user update admin --user_pass=newpassword + +# Reset password by email +wp --allow-root user update user@email.com --user_pass=newpassword + +# If WAF is blocking login page independently of plugin: +# 1. Remove Wordfence WAF block from .htaccess +# 2. Rename wordfence-waf.php to wordfence-waf.php.disabled +# 3. docker restart wordpress +``` + +--- + +## 🔜 Next Steps + +- [ ] Wire WordPress publish webhook → n8n workflow → insert into `pbs_recipes` +- [ ] Build one-time migration workflow — n8n datatables → MySQL +- [ ] Finish reply workflow refactor — remove Merge node, single `instagram_posts` lookup +- [ ] Fix production WordPress admin email (same issue exists there!) +- [ ] Deploy all changes to production once staging is verified + +--- + +## 📝 Notes + +- All development on staging (staging.plantbasedsoutherner.com) +- n8n datatables to be retired once MySQL migration complete +- Foreign key constraint requires `pbs_recipes` record to exist before `instagram_posts` insert +- Use `WHERE post_id IS NULL` query to find unmatched reels for Content Hub dashboard + +--- + +*Last Updated: March 16, 2026* +*Maintained by: Travis* +*Project: Plant Based Southerner — Instagram Automation* \ No newline at end of file