Updated end of session 2026-03-13 (evening). This file is the single source of truth for continuing work on this project.
Marketing HQ is Synup's internal marketing operations dashboard. It's the hub for all marketing tools, starting with a Google Ads auditor that runs on-demand or on a user-configured schedule, presenting actionable recommendations.
| Component | Tech | Location | Purpose |
|---|---|---|---|
| Dashboard | Next.js 15 + TypeScript + Tailwind | Vercel (marketing-hq-nine.vercel.app) |
UI for team (@synup.com only) |
| Database | Supabase (PostgreSQL) | Supabase cloud | Stores audit results, campaign data, user profiles, schedules |
| Auditor | Python (google-ads library) | DO droplet (167.71.229.75) | 74-check Google Ads audit engine |
| Poller | poll_audit_requests.py (cron */5) | DO droplet | Polls for on-demand + scheduled audits every 5 min |
| Auth | Supabase Auth + Google OAuth | Supabase | Restricted to @synup.com emails |
| API | Supabase REST API | Supabase | For Clawbot / CEO daily data access (service_role key) |
markops/
├── marketing-hq/ ← Next.js dashboard (this project)
│ ├── src/
│ │ ├── app/ ← Next.js App Router pages
│ │ │ ├── (auth)/login/ ← Google OAuth login
│ │ │ ├── (dashboard)/ ← Protected dashboard routes
│ │ │ │ ├── audit/ ← Adwords audit view (6 tabs: Search Terms, Negatives, Expansion, Pause, Issues, Activity Log)
│ │ │ │ ├── campaigns/ ← Campaign analytics (pulls from campaign_metrics or audit report fallback)
│ │ │ │ ├── keywords/ ← Negative keyword management
│ │ │ │ └── settings/ ← User profile + Audit Scheduler + User Management (admin only)
│ │ │ └── api/auth/ ← OAuth callback
│ │ ├── components/
│ │ │ ├── ui/ ← Reusable UI atoms (ScoreGauge, StatCard, StatusBadge, etc.)
│ │ │ ├── layout/ ← Sidebar, Topbar
│ │ │ └── features/
│ │ │ ├── audit/ ← AuditScoreHeader, AuditTriggerButton, SearchTermsPanel, SearchTermRow, ExpansionPanel, PausePanel, ActionLogPanel, ActionLogRow
│ │ │ ├── campaigns/ ← CampaignTable
│ │ │ ├── keywords/ ← NegativeKeywordRow, NegativeKeywordsList, KeywordExpansionRow
│ │ │ ├── schedule/ ← ScheduleDisplay, ScheduleForm, TimezoneSelect
│ │ │ └── users/ ← UserManagement, UserRow
│ │ ├── hooks/ ← useAuth, useAuditData, useAuditTrigger, useAuditSchedule, useCampaigns, useSearchTerms, useUsers, useKeywordActions
│ │ ├── lib/supabase/ ← Supabase client configs (browser, server, admin)
│ │ └── types/ ← TypeScript interfaces
│ ├── supabase/
│ │ ├── migrations/
│ │ │ ├── 001_initial_schema.sql ← 7 tables + RLS + auth trigger
│ │ │ ├── 002_search_terms_and_audit_requests.sql ← 3 tables (audit_requests, search_terms, search_term_summaries)
│ │ │ ├── 003_audit_schedules.sql ← 1 table (audit_schedules)
│ │ │ ├── 004_admin_and_campaigns.sql ← Set niladri as admin + admin RLS policies
│ │ │ └── 005_keyword_action_log.sql ← Audit trail table + decided_at column + insert policy safety
│ │ ├── push_to_supabase.py ← Pushes audit JSON → Supabase (6 sections)
│ │ ├── poll_audit_requests.py ← Polls for on-demand + scheduled audits
│ │ └── run_weekly_audit.sh ← Legacy cron runner (replaced by scheduler)
│ ├── MARKETING_HQ_USER_GUIDE.docx ← End-user guide (9 sections: Getting Started, Dashboard, Audits, Keywords, Campaigns, API/Clawbot, Settings, Troubleshooting)
│ ├── MARKETING_HQ_ROADMAP.docx ← Product roadmap + contributor best practices + AI assistant instructions (12 pages)
│ ├── CLAWBOT_API.md ← REST API docs for CEO bot / Clawbot integration
│ └── SETUP_GUIDE.md
│
├── google_ads_auditor/ ← Python audit engine
│ ├── auditor.py ← 74-check scoring engine
│ ├── run_audit.py ← CLI orchestrator
│ ├── google_ads_client.py ← Google Ads API client (patched: campaign.status in fetch_extensions SELECT)
│ ├── search_term_analyzer.py ← Negative keyword detection
│ ├── report_json.py ← JSON output (consumed by Supabase pusher)
│ ├── report_excel.py ← XLSX reports
│ ├── report_pdf.py ← PDF executive summary
│ ├── email_sender.py ← Gmail SMTP (currently unused)
│ └── config/config.yaml ← Audit thresholds & settings (credentials via env vars)
│
└── marketing-hq-legacy/ ← Original static HTML dashboard (deprecated)
└── index.html ← 2,219-line monolith
- Theme: Dark mode only
- Brand color:
#7C3AED(purple) - Background:
#0C0C0C→#141414→#1A1A1A(3-level depth) - Text:
#FFFFFF/#888888/#444444 - Status colors: Green
#22C55E, Red#EF4444, Yellow#F59E0B, Orange#F97316 - Font: System fonts (-apple-system, Inter)
- Component rule: No component > 150 lines. UI separated from logic via hooks.
| Table | Purpose | Written By |
|---|---|---|
profiles |
User info (auto-created on signup, role: admin/editor/viewer) | Supabase Auth trigger |
audit_runs |
Audit scores, categories, issues, raw_report JSON | Droplet (service_role) |
negative_keywords |
Candidate terms to block (status: candidate→approved/denied→pushed) | Droplet (service_role) + Dashboard users |
keyword_expansions |
Candidate terms to add | Droplet (service_role) |
keywords_to_pause |
Underperforming keywords | Droplet (service_role) |
campaign_metrics |
Daily campaign snapshots (backfilled 30 days, daily cron at 2am) | Droplet (service_role) |
change_log |
History of pushed changes | Dashboard users |
audit_requests |
On-demand audit triggers | Dashboard users → droplet processes |
search_terms |
All search terms with type classification | Droplet (service_role) |
search_term_summaries |
Per-run search term rollups | Droplet (service_role) |
audit_schedules |
User-configured recurring audit schedules | Dashboard users → droplet checks |
keyword_action_log |
Audit trail: who did what, when, with undo support | Dashboard users |
push_requests |
Push-to-Ads requests (pending→processing→completed/failed) | Dashboard → Droplet |
- Search Terms tab: User reviews search terms, multi-selects negative/wasted spend candidates → "Add to Negative Candidates" button inserts into
negative_keywordswith statuscandidate - Negative Keywords tab: User reviews candidates, multi-selects → bulk "Approve All" or "Deny All" (or individual approve/deny)
- Activity Log tab: Shows all actions with timestamps, user info, and undo buttons. Actions: added_as_candidate, approved, denied, bulk_approved, bulk_denied, undone, pushed_to_ads
- Push to Ads: Approved keywords get pushed to Google Ads API via
push_negatives_to_ads.py(triggered from dashboard "Push to Ads" button →push_requeststable → droplet polls and executes)
- Admin: Can view all users, change roles, manage schedules. niladri@synup.com is admin.
- Editor: Can approve/deny keyword candidates (future).
- Viewer: Read-only dashboard access. Default role for new signups.
- Self-service join: Any @synup.com user can visit the app URL and sign in with Google. Auto-created as Viewer.
- Project name: Adwords
- URL:
https://bgxgukkriymmtlzkkjkg.supabase.co - Migrations 001–007 have all been run
- Google OAuth configured with @synup.com domain restriction
- Redirect URLs:
https://marketing-hq-nine.vercel.app/api/auth/callbackandhttp://localhost:3000/api/auth/callback
- Production URL:
https://marketing-hq-nine.vercel.app - Project name:
marketing-hq(under Synup's projects) - Connected to GitHub:
synup/markopsrepo (auto-deploys on push) - Environment variables set:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY(all environments) - Note:
SUPABASE_SERVICE_ROLE_KEYis NOT on Vercel (not needed — only used on droplet)
- IP:
167.71.229.75(Ubuntu 22.04, Bangalore) - Project dir:
/opt/google-ads-auditor - Deployed scripts:
push_to_supabase.py,poll_audit_requests.py,push_negatives_to_ads.py,fetch_campaign_metrics.py - Cron:
*/5 * * * *polls for on-demand audits + scheduled audits + push-to-ads requests - Cron (daily 2am):
fetch_campaign_metrics.py --days 1(sources .env, logs to /var/log/campaign_metrics.log) - Swap: 1GB swap file added (droplet only has 1GB RAM)
- Env vars in
/opt/google-ads-auditor/.env: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, GOOGLE_ADS_CUSTOMER_ID, GOOGLE_ADS_LOGIN_CUSTOMER_ID, plus Google Ads OAuth credentials - Google Ads account: Synup USA - Agency (185 campaigns, 36,896 keywords)
- Patch applied:
google_ads_client.pyline ~371 — addedcampaign.statusto fetch_extensions SELECT clause
- Next.js project scaffolded with all core files
- Supabase database with 14 tables (migrations 001-005 run, 006+007 pending)
- Google OAuth working (tested — login successful on both localhost and Vercel)
- Dashboard pages: Home, Audit (8 tabs), Campaigns, Keywords, Settings
- Data hooks: useAuth, useAuditData, useAuditTrigger, useAuditSchedule, useCampaigns, useSearchTerms, useUsers, useKeywordActions, usePushToAds, useChangelog
- Push-to-Supabase script with search terms support (key mismatch fixed)
- On-demand audit trigger (dashboard → audit_requests → droplet polls)
- Scheduler UI (frequency/day/time/timezone picker on Settings page)
- User management UI (admin-only, role promotion/demotion)
- Self-service join for @synup.com users (auto-created as Viewer)
- Poll script handles both on-demand and scheduled audits
- Live audit run successful (audit_run_id=3, score 62.5/100, 407 negative candidates, $894 wasted spend)
- Campaigns page with fallback to audit report data when campaign_metrics table is empty
- Droplet: 1GB swap added, cron active, all scripts deployed
- niladri@synup.com set as admin
- Vercel deployment live at
marketing-hq-nine.vercel.app - TypeScript build errors fixed: cookie types, audit history types, profile types
- Null safety fixes:
.single()→.maybeSingle()across all Supabase queries - JSON parsing safety:
categories,critical_issues,quick_winsnow handled as arrays or strings - Auth callback improved: Shows specific error messages
- Security audit passed: No hardcoded secrets
- Multi-select approval flow: Search Terms → select candidates → "Add to Negative Candidates" (inserts as
candidatestatus) - Negative Keywords multi-select: Bulk approve/deny with floating action bar
- Audit trail system:
keyword_action_logtable,useKeywordActionshook (logAction, logBulkActions, undoAction),useActionLoghook - Activity Log UI: New tab in Audit page showing all actions with timestamps, user names, undo buttons
- Error feedback: SearchTermsPanel shows error messages when inserts fail
- decided_at tracking:
useAuditData.updateStatusnow recordsdecided_byanddecided_at - 9/407 bug fixed:
push_to_supabase.pywas reading wrong JSON key (negative_candidatesvsnegative_keyword_candidates); also fixed for expansions; removed arbitrary caps (50/30) - Push-to-Ads:
push_negatives_to_ads.pyscript pushes approved negative keywords to Google Ads viaCampaignCriterionService.mutate_campaign_criteria(); dashboard has "Push to Ads" button usingpush_requestspolling table - Campaign metrics fetcher:
fetch_campaign_metrics.pyfetches daily campaign snapshots from Google Ads API; supports backfill with--days N - Changelog tab: Shows push history and change details with status badges
- Clawbot API docs:
CLAWBOT_API.mddocuments all Supabase REST API endpoints for the CEO bot - User Guide:
MARKETING_HQ_USER_GUIDE.docx— 9-section guide covering sign-in, dashboard, audit workflow, keyword management, campaign metrics, API key generation, Clawbot integration, settings, and troubleshooting
- Migrations 006+007 run in Supabase
- All scripts deployed to droplet (push_negatives_to_ads.py, fetch_campaign_metrics.py, poll_audit_requests.py, push_to_supabase.py, report_json.py)
- Daily cron at 2am for campaign metrics (sources .env)
- Full audit re-run: 402 negative candidates, 7 expansion candidates, 409 search terms pushed (audit_run_id=10)
- Campaign metrics backfilled (30 days, 44 records)
- Vercel auto-deployed on git push
- Type casting fixes in push_to_supabase.py (int/float for Supabase integer/numeric columns)
- Search term key consistency fix (all objects have matching keys for Supabase bulk insert)
- fetch_campaign_metrics.py: fixed module import path + added env var overrides
- Audit runs 6-9 in Supabase have partial/duplicate data from debugging — delete from dashboard
- Gmail App Password on droplet is only 10 chars (needs 16) — but email is no longer needed
- Legacy
Downloads/Adwords auditor/path in repo should be restructured - Negative keywords are campaign-level only (no account-level shared lists in the current schema)
| What | Where |
|---|---|
| GitHub repo | github.com/synup/markops |
| Vercel deployment | marketing-hq-nine.vercel.app (project: marketing-hq) |
| DO Droplet | 167.71.229.75 (Ubuntu 22.04, $6/mo, Bangalore) |
| Droplet project dir | /opt/google-ads-auditor |
| Google Ads OAuth | Desktop app credentials in droplet .env |
| Google Ads Account | Synup USA - Agency (Customer ID in droplet .env) |
| Supabase project | https://bgxgukkriymmtlzkkjkg.supabase.co (project: Adwords) |
- TypeScript cookie types in middleware, server, callback
- AuditHistory pick type for partial select
- Profile card
nullvsundefinedtypes .single()→.maybeSingle()across all hooks- JSON array safety for categories/issues
- Auth callback specific error messages
- Multi-select SearchTermsPanel with floating action bar
- SearchTermRow with checkbox, "Already Added" badge, click-to-select
- NegativeKeywordsList with multi-select, bulk approve/deny, status filter
- NegativeKeywordRow with checkbox for candidates
keyword_action_logtable (migration 005) for full audit trailuseKeywordActionshook: logAction, logBulkActions, undoActionuseActionLoghook: fetches history with performer info- ActionLogPanel + ActionLogRow components: Activity Log tab with undo buttons
- Error feedback in SearchTermsPanel for failed inserts
decided_at/decided_bytracking on keyword status updates
- Deployed all 5 Python scripts to droplet via python3 heredoc + base64 methods
- Fixed
push_to_supabase.py: int()/float() casting for Supabase integer columns - Fixed
push_to_supabase.py: all search_term objects now have matching keys (Supabase PGRST102 fix) - Fixed
report_json.py: removed [:50]/[:30] caps on both droplet copies - Fixed
fetch_campaign_metrics.py: module import path (google_ads_auditor.google_ads_client) + env var overrides - Both cron jobs now source
.envbefore running - Full audit re-run: 402 negatives + 409 search terms pushed (audit_run_id=10)
- Campaign metrics backfilled: 44 records across 30 days
- Created
MARKETING_HQ_USER_GUIDE.docx— professional user guide with API key generation + Clawbot setup instructions - Created
MARKETING_HQ_ROADMAP.docx— 12-page roadmap with 3 phases (Analytics Foundation, Content & Lead Funnel, Automation & Workflows), contributor best practices, new feature checklist, data source pattern, database migration guide, droplet deployment guide, and AI assistant instructions
- Components < 150 lines — split if exceeding
- UI separate from logic — hooks for data, components for display
- Save context — update this file and push to GitHub at end of every session
- Use subagents — for exploration, research, multi-file analysis
- Return summaries — not raw data from subagents
- Keep droplet — it runs the Python auditor + poller (not GitHub Actions)
- Credentials in .env only — never in config files or chat
- Use
.maybeSingle()— never.single()for Supabase queries that might return 0 rows - Test locally before deploying — run
npm run build(ornpx tsc --noEmit) to catch TypeScript errors before deploying - Read the roadmap — new contributors and AI assistants should read
MARKETING_HQ_ROADMAP.docxbefore building new features - Feature branches only — never commit directly to main; create
feature/your-feature-nameand merge via PR - Follow the 4-layer pattern — new data sources = new table + new fetcher + new hook + new page (see roadmap Section 2.4)