Commit graph

36 commits

Author SHA1 Message Date
35c6e5f7bc feat(discovery): add Lemmy community search, fix dead request, add platform field
- Add app/services/lemmy/discovery.py: searches 5 major Lemmy instances,
  deduplicates by actor_id (AP canonical URL), skips NSFW communities,
  uses community@instance naming convention matching existing Lemmy client
- Update POST /subs/discover: accepts platforms[] param (default both),
  fans out to Reddit + Lemmy search, merges and sorts by subscribers
- Add platform field to all discovery result dicts (Reddit and Lemmy)
- Fix: remove dead _get() call left in search_subs() during earlier refactor
- Frontend: show platform badge on each discovery row, correct hyperlink
  format for Lemmy (https://{instance}/c/{community}), pass r.platform
  to upsertRules on import so Lemmy subs land in the lemmy platform slot
2026-06-13 22:23:31 -07:00
f39f36e258 feat(discovery): subreddit discovery and rule classification (#2)
- Add app/services/reddit/discovery.py:
  - search_subs(): searches /subreddits/search.json by keyword
  - analyze_sub(): fetches /about.json + /about/rules.json per sub
  - _classify_rules(): keyword-pattern classifier for promo policy
    (banned / conditional / unknown; hard to positively confirm allowed)
  - search_and_analyze(): combined search + per-sub analysis entry point
  - Unauthenticated-friendly (uses auth cookies when available)
- Add POST /subs/discover endpoint: returns candidate list with
  promo_allowed, flair_required, subscriber count, notes excerpt,
  and already_tracked flag. Nothing stored until user imports.
- Add SubDiscoveryResult interface and api.subs.discover() in api.ts
- Rework SubRulesView: slide-in discovery panel (right drawer),
  per-row Import button, auto-marks already-tracked subs, immutable
  result update on import

Closes: #2
2026-06-13 22:17:53 -07:00
dfdde692b8 feat(engagement): poll Reddit post metrics after posting (#6)
- Add RedditClient.fetch_stats() — fetches score/upvotes/comments/awards via by_id API
- Add Store.list_posts_needing_poll() — selects successful Reddit posts not checked within recheck window
- Add Store.list_posts() LEFT JOIN latest engagement snapshot (avoids N+1 on frontend)
- Add app/services/engagement.py — poll_recent_posts() async service with unauthenticated fallback
- Register hourly engagement poll job in APScheduler at startup
- Add POST /posts/poll-engagement for manual triggers
- Update Post interface with engagement fields (score, comment_count, awards, engagement_checked_at)
- Add Score/Comments columns and poll button to PostsView

Closes: #6
2026-06-13 22:02:07 -07:00
15779e3114 fix(session): pass REDDIT_SESSION_FILE env to post.py subprocess; fix status check path
refresh_session() was spawning post.py --login without the REDDIT_SESSION_FILE env
var, so Playwright wrote the refreshed session to the legacy session.json default
instead of sessions/alan_reddit.json. The poster then read a stale file and
failed with 'Playwright re-login failed'.

- Add os import; pass env={...os.environ, REDDIT_SESSION_FILE: str(session_file)}
  to subprocess.run so post.py writes to the correct per-account path
- manage.sh status now checks sessions/alan_reddit.json (canonical) not session.json
- Replace session.json with a symlink to sessions/alan_reddit.json for legacy compat
2026-06-01 15:37:35 -07:00
e9b4cdd88e feat: link_url variants, team accounts, session layout, menagerie route (#18 #19)
#19 — link_url on campaign variants (migration 019)
- ADD COLUMN link_url TEXT on campaign_variants
- create_variant, upsert_variant, update_variant all carry link_url
- RedditClient.post() supports kind=link when link_url set + body empty
- RedditPostStrategy passes link_url from extra dict
- poster.py merges link_url from variant into extra (same as slug/tags)
- API VariantCreate/VariantUpdate schemas include link_url
- CampaignDetail: link_url field in Add Variant form with copy button;
  link_url shown in variant list with clickable link + copy button
- Variant button disabled if neither body nor link_url is set

#18 — Multi-user team accounts (migrations 020-022)
- 020: team_accounts table (display_name, platform, username, session_file)
- 021: opportunities.assigned_to + post_as FK → team_accounts
- 022: posts.posted_by_account_id FK → team_accounts
- Store: list/get/get_by_username/create_team_account, assign_opportunity
- API: GET/POST /api/v1/team; POST /api/v1/team/{id}/assign
- config.py: sessions_dir added; reddit_session_file now points to
  sessions/alan_reddit.json (backward compat path kept)
- scripts/migrate_sessions.py: one-shot move session.json →
  sessions/alan_reddit.json + creates placeholder files for future accounts
- manage.sh: build (VITE_BASE_URL=/magpie/ npm build), serve (static),
  migrate-sessions subcommands added; login updated to new session path
- Caddy: @magpie_no_session gate + handle /magpie/api* and /magpie*
  blocks added to menagerie.circuitforge.tech site block
2026-05-27 15:31:58 -07:00
a863960266 feat: structured logging, frontend error toasts, stats bar (#14 #15 #16)
#14 — Structured logging
- app/core/logging_config.py: configure_logging() sets stdout handler
  with timestamped format; called at import time in main.py
- Global FastAPI exception_handler logs 500s with full traceback
- opportunities.py: logger added; create/approve/mark-posted/dismiss
  each emit an info line so failures are traceable

#15 — Frontend error handling
- frontend/src/composables/useToast.ts: shared toast composable
  (error/success/info, auto-dismiss, module-level singleton)
- frontend/src/components/ToastList.vue: fixed-position overlay,
  theme-aware, accessible (role=alert, aria-live=polite)
- OpportunitiesView: all 6 async actions have catch + toast.error()
- CampaignDetail: onMounted + all 6 mutation functions wrapped

#16 — Aggregate stats
- app/api/endpoints/stats.py: GET /api/v1/stats — single DB pass
  via GROUP BY; returns posts totals, 7-day count, top communities,
  platform breakdown, and opportunity queue counts
- frontend/src/components/StatsBar.vue: slim header bar above
  router-view; chips for posts ok/failed/week, queue pending/approved/
  posted, top community; hides gracefully on API error
2026-05-25 15:02:15 -07:00
c2f036ab21 feat: Plan C — BlogPostStrategy and migration 017
Adds blog_post campaign type that publishes directly to the CircuitForge
website via Directus CMS. Implements PostingStrategy ABC with:
- supports_dupe_guard() → False (blog posts run on demand, no weekly guard)
- execute() → wraps publish_blog_post() from directus.py
- tags stored as JSON string in DB, decoded at post time

Migration 017 adds slug, tags, seo_description columns to campaign_variants.
Poster merges variant blog fields into extra dict before strategy execution.
Seed script updated with blog campaign (id=9, target=blog/main).
Registry updated; 6 new unit tests added.
2026-05-25 14:43:43 -07:00
3407ebff66 fix: extend dupe guard window to 30 days by default
Weekly check was too narrow for bi-weekly and monthly campaigns.
Parametric days= arg lets callers override for tighter schedules.
2026-05-25 14:43:33 -07:00
5f44ad66a5 feat(db): migration 017 — make campaign_id nullable on posts for manual opportunity posts 2026-05-06 08:52:33 -07:00
a2620570fa feat(dupe-guard): add max_posts per-sub cap to prevent one-shot intro campaigns from re-posting
Adds max_posts INTEGER to campaign_subs (NULL = unlimited/evergreen).
Adds successful_post_count() query counting lifetime success records.
poster.py checks max_posts before the 7-day rolling dupe guard.

Root cause: campaign 2 fired 8 days after the last post (just outside the
7-day window), allowing a duplicate r/opensource pitch. Fix: set max_posts=1
on intro campaigns so the lifetime cap fires regardless of window.
2026-05-06 08:52:21 -07:00
774fbb37c3 feat: pass variant blog fields (slug, tags, seo_description) to strategy extra dict 2026-04-27 16:11:04 -07:00
e7316d177f feat: register BlogPostStrategy in platform registry 2026-04-27 16:10:15 -07:00
65fd09c06d feat: add BlogPostStrategy wrapping Directus publish_blog_post() 2026-04-27 16:09:35 -07:00
524b05ef8b feat: add slug, tags, seo_description columns to campaign_variants for blog posts 2026-04-27 14:50:18 -07:00
01e5990f58 fix: replace Playwright post() with httpx legacy API, fix session_file Path coercion and comment URL check
smoke test confirmed: post via /api/submit + comment via /api/comment both working with modhash + session cookies
2026-04-27 14:25:38 -07:00
6cf61663a5 chore: delete dead platforms.py and fix seed_campaigns mutation bug 2026-04-27 13:03:57 -07:00
a3932aef1e fix: handle ValueError from parse_occurrence and add edge-case occurrence tests
- Wrap parse_occurrence() call in try/except ValueError; return skipped with reason instead of crashing
- Remove redundant `or {}` guard on sub_row (already defaulted to {} via next(..., {}))
- Strengthen test_occurrence_passes assertion to check status == "success"
- Add 3 edge-case tests: occurrence="every", missing occurrence key, invalid occurrence string
2026-04-27 12:57:44 -07:00
08aa019439 feat: add occurrence check to poster before strategy dispatch
Parse the occurrence field from sub_row and skip execution when today
is not the nth weekday specified (e.g. first_sunday). Check runs after
sub_row fetch but before dupe guard. Two new tests confirm skip and
pass paths using patched date.today in app.services.poster.
2026-04-27 12:27:16 -07:00
90d30167f8 fix: add timeout and error wrapping to _find_sticky, clean up test fixture 2026-04-27 12:23:44 -07:00
e37be0935d feat: implement RedditCommentStrategy and register in platform registry
Adds RedditCommentStrategy to app/services/platforms/reddit_comment.py,
resolving thread_id via thread_url_override or _find_sticky title search,
falling back to reconstructed URL when client.comment() returns empty string.
Registers the strategy under "reddit_comment" in the platform _REGISTRY.
7 new tests confirm all execution paths: url override, title pattern lookup,
not-found error, missing-extra error, empty-URL reconstruction, dupe guard,
and registry presence. Full suite: 34/34 passing.
2026-04-27 12:00:08 -07:00
719a1d5aca fix: address code review issues in reddit_comment thread detection helpers 2026-04-27 11:55:55 -07:00
9d955b2c50 feat: add thread detection helpers to reddit_comment strategy 2026-04-27 11:36:36 -07:00
ca9b2ac0b2 feat: add is_nth_weekday() and parse_occurrence() for scheduled comment gating 2026-04-27 11:04:30 -07:00
a06582c028 feat: add store helpers and seed r/Flipping + r/cscareerquestions comment campaigns 2026-04-27 11:00:11 -07:00
9248410cf1 feat: add comment config columns to campaign_subs (thread_title_pattern, thread_url_override, occurrence) 2026-04-27 10:43:05 -07:00
81a63ab0ec refactor: dispatch poster by campaign.type via platform strategy registry
Replace hardcoded platform dispatch with get_client(campaign["type"]) so
any future campaign type (blog_post, email, etc.) routes automatically
through the strategy registry. Adds dupe-guard opt-out per strategy,
sub_row pre-fetch for extra metadata, and 5 new TDD tests (14 total).
2026-04-27 08:38:12 -07:00
de6dc645f6 feat: add platform registry with get_client() dispatch 2026-04-27 08:12:47 -07:00
38d212726e feat: add type param to store.create_campaign() (default reddit_post) 2026-04-27 08:09:41 -07:00
ae96621f6c feat: add RedditPostStrategy wrapping RedditClient.post() 2026-04-27 08:09:03 -07:00
2dd88285a2 feat: add PostingStrategy ABC and PostResult dataclass 2026-04-27 08:00:36 -07:00
e158787b59 feat: add type column to campaigns (default reddit_post) 2026-04-27 07:53:54 -07:00
c7c57fe4e5 feat: opportunities UI improvements, MCP tools, session refresh, migrations 013-014 2026-04-27 07:49:34 -07:00
add5475d50 feat: add Directus blog post publisher and MCP tool
- app/services/directus.py: Directus CMS client using docker run
  curlimages/curl on website_cf-internal network; supports static admin
  token with fresh JWT login fallback; get/publish/update blog_posts
- app/api/endpoints/blog.py: POST /api/v1/blog (publish), GET /slug,
  PATCH /id endpoints
- app/api/routes.py: register blog router
- app/core/config.py: add directus_url/token/email/password/network settings
- mcp/server.js: add publish_blog_post and get_blog_post MCP tools

Key gotcha: Directus filter[field][_eq] brackets must be percent-encoded
when passed as a curl CLI URL arg — raw brackets cause curl to exit
non-zero with empty stderr.
2026-04-26 14:14:35 -07:00
a6ea0b9c58 feat(#7,#10): signal crawler -- Reddit + Lemmy community monitoring
Implements the full signal detection pipeline:

Backend:
- app/services/lemmy/client.py: async Lemmy API v3 client, community@instance
  addressing, integer cursor dedup, normalised post dicts
- app/services/scraper.py: platform-agnostic scraper; Reddit (.json API,
  fullname cursor) + Lemmy (integer ID cursor); keyword/regex/all match modes,
  min_score gate, NormalizedPost shape, upsert dedup via UNIQUE post_id
- app/api/endpoints/signals.py: CRUD for signal_rules + signals queue;
  POST /signals/scrape manual trigger; scrape-state viewer
- migrations 010-012: signal_rules, signals, signal_scrape_state tables
- scheduler: interval job every 30 min (scraper_enabled=True in config)
- Fixed migration collision: 007_signal_rules.sql → 010, 008 → 011, 009 → 012

Frontend:
- SignalsView.vue: signal feed with status filter (new/saved/dismissed),
  keyword chips, score/comment counts, save/dismiss actions, rules editor panel
- api.ts: SignalRule, Signal types + signalRules/signals API methods
- Nav: Signals as default landing route (replaces /campaigns default)

Closes #7 (signal extraction), closes #10 (Lemmy JSON crawler)
2026-04-22 11:00:14 -07:00
2822d36bad feat(#9): opportunities queue — manual posting workflow UI and API
Adds the full signal-to-post pipeline for non-automated opportunities:
- SQLite migration 007: opportunities table (platform, community, thread_url,
  draft title/body, post_type, status, campaign_id, dismiss_note)
- FastAPI endpoints: GET/POST /opportunities, GET/PATCH /{id}, /{id}/approve,
  /{id}/mark-posted, /{id}/dismiss
- approve() returns auto_post_ready (Reddit) or manual_handoff (Lemmy/LinkedIn/etc)
  with clipboard-ready draft and instructions
- OpportunitiesView.vue: status-filtered queue, slide-over detail panel with
  inline draft editor, approve/dismiss actions, manual handoff copy+open flow
- Opportunities now default landing route; nav link added
- MCP tools: list_opportunities, create_opportunity, approve_opportunity,
  dismiss_opportunity, update_opportunity

Closes #9
2026-04-21 16:51:34 -07:00
2cc85d8fc5 feat: scaffold Magpie — campaign scheduler + social posting platform
FastAPI backend (SQLite + APScheduler), Vue 3 frontend, MCP server for
Claude integration, and Docker Compose stack. Includes campaign data model
(campaigns → variants → subs), post history, sub rules, and Playwright-based
Reddit posting layer migrated from claude-bridge/reddit-poster.

Also seeds legacy campaigns (6) and sub rules (14) from reddit-poster history.

Closes #1 (scaffold), resolves migration from claude-bridge.
2026-04-21 16:51:33 -07:00