#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
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.
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.
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.
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.
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).
- 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.
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.