Skip to content

Social Cards

Garot Conklin edited this page Jun 1, 2026 · 2 revisions

Social Cards (Bluesky)

Signal automatically generates three infographic cards each morning and posts them to Bluesky at scheduled times. Each card is a 1200×630px PNG rendered from an HTML template and posted with a short teaser that links back to the full report.

Command runbook: This page is the full how-to (every shell command). The same text lives in the repo as docs/social-cards-setup.md. Design spec: docs/signal_social_feature.md.


Command runbook

All commands from the repo root:

cd /Users/garotconklin/garotm/fleXRPL/signal

Always use .venv/bin/pip and .venv/bin/python — not system pip (macOS blocks it).

1. Install dependencies

.venv/bin/pip install playwright atproto python-dotenv
.venv/bin/playwright install chromium

2. Bluesky app password and .env

  1. bsky.app → your account → Settings → Privacy and Security → App Passwords
  2. Add App Password (e.g. name: signal-pipeline) — copy the xxxx-xxxx-xxxx-xxxx value once
  3. Do not put your normal login password in .env
cp .env.example .env

Edit .env:

BLUESKY_HANDLE=yourhandle.bsky.social
BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx

Confirm .env never appears in git status.

3. Generate cards (daily pipeline)

Cards are created during the 5:00 AM daily run, not at post time.

.venv/bin/python main.py

Or after the scheduled run, verify today's files (UTC date):

DATE=$(date -u +%Y%m%d)
ls -la reports/cards/am_${DATE}.png reports/cards/noon_${DATE}.png reports/cards/pm_${DATE}.png
ls -la reports/posts/am_${DATE}.json reports/posts/noon_${DATE}.json reports/posts/pm_${DATE}.json

If missing: grep -i social logs/cron.log | tail -20

4. Dry run (no post to Bluesky)

.venv/bin/python post_scheduled.py --slot am --dry-run
.venv/bin/python post_scheduled.py --slot noon --dry-run
.venv/bin/python post_scheduled.py --slot pm --dry-run

Specific date:

.venv/bin/python post_scheduled.py --slot am --date 20260601 --dry-run

5. Post manually

.venv/bin/python post_scheduled.py --slot am
.venv/bin/python post_scheduled.py --slot noon
.venv/bin/python post_scheduled.py --slot pm

Re-post: set "posted": false in reports/posts/{slot}_YYYYMMDD.json, then run the command again.

6. Install launchd jobs (one-time)

cp scripts/com.flexrpl.signal.social.am.plist   ~/Library/LaunchAgents/
cp scripts/com.flexrpl.signal.social.noon.plist ~/Library/LaunchAgents/
cp scripts/com.flexrpl.signal.social.pm.plist   ~/Library/LaunchAgents/

launchctl load ~/Library/LaunchAgents/com.flexrpl.signal.social.am.plist
launchctl load ~/Library/LaunchAgents/com.flexrpl.signal.social.noon.plist
launchctl load ~/Library/LaunchAgents/com.flexrpl.signal.social.pm.plist

Verify all Signal jobs:

launchctl list | grep flexrpl

You should see daily, weekly, and three social.* entries. See also Scheduling for daily/weekly setup.

7. Fire a social job now (optional)

launchctl start com.flexrpl.signal.social.am
launchctl start com.flexrpl.signal.social.noon
launchctl start com.flexrpl.signal.social.pm

8. Logs

tail -f logs/social_am.log
tail -f logs/social_noon.log
tail -f logs/social_pm.log
tail -f logs/cron.log    # card generation during daily pipeline

9. Reload or unload plists

After editing scripts/com.flexrpl.signal.social.*.plist:

launchctl unload ~/Library/LaunchAgents/com.flexrpl.signal.social.am.plist
cp scripts/com.flexrpl.signal.social.am.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.flexrpl.signal.social.am.plist

Disable automated social posting:

launchctl unload ~/Library/LaunchAgents/com.flexrpl.signal.social.am.plist
launchctl unload ~/Library/LaunchAgents/com.flexrpl.signal.social.noon.plist
launchctl unload ~/Library/LaunchAgents/com.flexrpl.signal.social.pm.plist

Post schedule

Slot Time Card Content source
AM 7:00 AM Watch List watch_items from brief_data.json
Noon 12:00 PM Spectrum Breakdown top_cluster framing from brief_data.json
PM 6:00 PM Blindspot Analysis left_only / right_only from brief_data.json

All three cards are generated during the 5:00 AM pipeline run. The posting jobs do no model or rendering work — they simply load pre-generated files and post.


How it works

5:00 AM — main.py
    → Passes 1–5: analysis pipeline
    → reporter.py writes:
        reports/brief_YYYYMMDD_HHMM.html    (full report)
        reports/brief_YYYYMMDD_HHMM.json    (structured card data)
    → infographic.py renders:
        reports/cards/am_YYYYMMDD.png
        reports/cards/noon_YYYYMMDD.png
        reports/cards/pm_YYYYMMDD.png
    → social.py writes:
        reports/posts/am_YYYYMMDD.json
        reports/posts/noon_YYYYMMDD.json
        reports/posts/pm_YYYYMMDD.json

7:00 AM — post_scheduled.py --slot am
    → loads reports/posts/am_YYYYMMDD.json
    → posts image + text to Bluesky
    → marks package as posted

12:00 PM — post_scheduled.py --slot noon
    → loads reports/posts/noon_YYYYMMDD.json → posts

6:00 PM — post_scheduled.py --slot pm
    → loads reports/posts/pm_YYYYMMDD.json → posts

If the morning pipeline run fails, all three JSON packages will not exist and the posting jobs exit cleanly with code 1 (logged, no crash, no retry).


Card designs

All cards use the Signal dark theme (IBM Plex Mono / IBM Plex Sans, #0f1117 background) at 1200×630px.

Watch List (AM — amber)

  • Header: amber dot · "SIGNAL // Watch List" · date
  • Subheader: item count + time horizon summary
  • Grid: up to 6 watch items, each with a color-coded time window badge
    • 24hr → red, 48hr → orange, 72hr → amber, 5-day → green
  • Footer: flexrpl.github.io/signal

Spectrum Breakdown (Noon — cyan)

  • Header: cyan dot · "SIGNAL // Political Intelligence" · article/source counts
  • Story block: top cluster headline with left red border accent
  • Spectrum bar: proportional distribution across 7 outlet categories
  • Coverage columns (3-up): Left frame / Center frame / Right frame with source tags
  • Omissions row: what each side is not saying
  • Footer: cluster count

Blindspot Analysis (PM — purple)

  • Header: purple dot · "SIGNAL // Blindspot Analysis" · "What each side isn't saying"
  • Subheader: most significant suppressed story
  • 2-column grid: Left-Only stories (blue) vs Right-Only stories (red)
  • Footer: source count

Files

File Purpose
pipeline/infographic.py Playwright HTML→PNG renderer; render_all_cards() entry point
pipeline/social.py Bluesky auth, image upload, post composition; post_slot() dispatcher
pipeline/templates/card_watch.html Jinja2 template for Watch List card
pipeline/templates/card_spectrum.html Jinja2 template for Spectrum Breakdown card
pipeline/templates/card_blindspot.html Jinja2 template for Blindspot Analysis card
post_scheduled.py CLI: --slot [am|noon|pm], --dry-run, --date YYYYMMDD
scripts/com.flexrpl.signal.social.am.plist launchd job — 7:00 AM
scripts/com.flexrpl.signal.social.noon.plist launchd job — 12:00 PM
scripts/com.flexrpl.signal.social.pm.plist launchd job — 6:00 PM
.env.example Credential template
reports/cards/ Generated PNG card images (committed, served via Pages)
reports/posts/ Pre-generated JSON post packages

Troubleshooting

Cards not generated after morning run: Check logs/cron.log for Social card generation failed. The most common cause is playwright not installed — run .venv/bin/playwright install chromium.

Post failed: "BLUESKY_HANDLE and BLUESKY_APP_PASSWORD must be set": The .env file is missing or not being read. Verify it exists at the repo root and contains both variables. The launchd plist passes SIGNAL_ENV_FILE pointing to it — python-dotenv loads it automatically via load_dotenv().

Post skipped: "already posted": Each post package tracks a posted: true flag. If you need to re-post, edit the JSON in reports/posts/ and set "posted": false.

Slot not found (exit code 1): The morning pipeline run did not complete successfully. Check logs/cron.log. The JSON package for that date does not exist, so the post job exits cleanly.

Clone this wiki locally