Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .conductor/settings.local.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"$schema" = "https://conductor.build/schemas/settings.repo.schema.json"

[scripts]
setup = "pip install -r requirements.txt && python manage.py migrate"
auto_run_after_setup = false

[scripts.run.dev]
command = "python manage.py runserver $CONDUCTOR_PORT"
default = true

[[preview_urls]]
name = "BrightBean Studio"
url = "http://localhost:$CONDUCTOR_PORT"

[prompts]
general = "Read README.md and Makefile before starting. Check the agency-os token budget at ~/Projects/agency-os/state/agent_token_budgets.json before spawning subagents or running heavy LLM work. Prefer free/local LLM for quick triage and mechanical fixes. Reserve premier design models for actual design work and only when token budget allows. Nothing gets sent, posted, deployed-to-client, or invoiced without Eric's explicit sign-off. Never push directly to main or master - use a feature branch and PR."
71 changes: 71 additions & 0 deletions brightbean-social-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# BrightBean Social Readiness — Plants and Chairs

Re-derived 2026-07-18 (kimi) after the original file was lost. Sources: ingestion script
`scripts/ingest_assets.py` (branch `fix-brightbean-llms-setup`), live filesystem audit, and a
full Airtable DAM scan (base `appjU8WwLSSaSNJxC`, table `🎞 Digital assets`).

## 1. Verified source directories (live audit 2026-07-18)

| Directory | Media files |
|---|---|
| `agency-os/output/social-ready-2026-06-25/feed` | 26 |
| `agency-os/output/content-pool-2026-06-23` | 255 (incl. `reels/` with the 2 mp4s) |
| `~/Downloads/pac_store-ites_3d/item_1` | 142 |
| `agency-os/reels` | 0 (empty; reels live in content-pool) |

All 30 files mapped in `ASSET_METADATA` are present on disk (30/30). The two mp4s were
found at `content-pool-2026-06-23/reels/`, not `agency-os/reels/`.

## 2. DAM audit (Airtable, 2026-07-18)

- 2,120 total records. 936 have valid local paths, 114 point to missing files, 1,070 have
no Local Path at all.
- Valid by project: UNIVERZE 358, PNC-MERCH 198, PC-1985 60, PC-WFF 56, PC-007B 31,
PC-FLEA 30.
- Missing by project: PNC 53, PC-1985 15, SJB-EXPLORER 12, PC-SEAVITA 9.
- Full dump: `/tmp/dam_audit.json` (regenerate via the Airtable API when needed).

## 3. Ingestion candidate set (30 files, all verified present)

- Hero photos: `IMG_1761.png` (Plants Over Chairs hero), `IMG_1634.png` (Plants and Jeeps
hero), both in `pac_store-ites_3d/item_1`.
- Reels: `new-01-screenprint-process.mp4` (screen-print process),
`new-03-beach-lifestyle.mp4` (studio/place), both in `content-pool-2026-06-23/reels/`.
- Social-ready feed set (26 PNGs in `social-ready-2026-06-25/feed`): s01-s10 heroes and
grids, v2 series (stone, payasotee, vanlife, dotcap, barlogo), v3 cards (petstore,
productos, mialma, scatter pink), post-2..5 (flowers, vanlife royal, payaso tote,
beanie), cap-dir3 streetwear, cap-dir4 AR.

Runbook: `python scripts/ingest_assets.py --dry-run` first, then real run. The script
dedupes by SHA-256, uploads to R2 via Django storage, and creates MediaAsset rows.
**Blocker before any run: rotate the exposed R2 credentials, then set the new pair as Fly
secrets (`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`).**

## 4. Eight-post draft calendar (drafts only; nothing posts without Eric)

Cadence Tue/Fri, starting after accounts connect. All copy is placeholder direction for
the brand check, not final copy.

| # | Date | Asset | Format | Direction |
|---|------|-------|--------|-----------|
| 1 | Tue 2026-07-21 | `s01-iconic-hero.png` | IG feed | Iconic cap hero. Re-introduce the drop. |
| 2 | Fri 2026-07-24 | `new-01-screenprint-process.mp4` | Reel | Process video: screen-print behind the scenes. |
| 3 | Tue 2026-07-28 | `v2-01-stone-hero.png` | IG feed | Stone colorway hero. |
| 4 | Fri 2026-07-31 | `s06-iconic-grid2.png` | IG feed | Grid/detail post. |
| 5 | Tue 2026-08-04 | `v2-02-payasotee.png` | IG feed | Payaso tee graphic feature. |
| 6 | Fri 2026-08-07 | `new-03-beach-lifestyle.mp4` | Reel | Lifestyle/place reel. |
| 7 | Tue 2026-08-11 | `cap-dir3-streetwear-post.png` | IG feed | Streetwear styling post. |
| 8 | Fri 2026-08-14 | `s08-scatter-sky.png` | IG feed | Scatter print, soft closer for the run. |

Crosspost: once Bluesky/Mastodon are connected, mirror 1, 3, 5, 7 as stills. Captions go
through the QA/brand gate and Eric approval before scheduling.

## 5. Account connection status (2026-07-18)

- Instagram: blocked at Meta SMS 2FA in the token-generation step (PAC-337, needs Eric).
App credentials (`PLATFORM_INSTAGRAM_APP_ID/SECRET`) are set as Fly secrets.
- Bluesky: no account yet. Needs studio handle + App Password (agent connects it).
- Mastodon: no instance/account yet.
- LinkedIn, Pinterest, TikTok, Google/YouTube: app credentials exist in
`brightbean-studio/.env`; each still needs its OAuth grant flow.
- R2/S3: Fly secrets present but the pair is exposed; rotate before uploads.
Binary file added brightbean.db
Binary file not shown.
9 changes: 7 additions & 2 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,13 @@

# Platform credentials env vars (cloud version)
_META_CREDENTIALS = {
"app_id": env("PLATFORM_FACEBOOK_APP_ID", default=""),
"app_secret": env("PLATFORM_FACEBOOK_APP_SECRET", default=""),
# The existing PnC Meta app is configured through the Instagram Business
# use case. Keep the dedicated Instagram variables as a compatibility
# fallback so an existing deployment can use the same app without a
# destructive secret rotation.
"app_id": env("PLATFORM_FACEBOOK_APP_ID", default="") or env("PLATFORM_INSTAGRAM_APP_ID", default=""),
"app_secret": env("PLATFORM_FACEBOOK_APP_SECRET", default="")
or env("PLATFORM_INSTAGRAM_APP_SECRET", default=""),
Comment on lines +365 to +367

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Instagram Login creds out of Meta OAuth

When a deployment follows the documented Instagram (Direct) setup and sets only PLATFORM_INSTAGRAM_APP_ID/PLATFORM_INSTAGRAM_APP_SECRET, this fallback also makes the shared _META_CREDENTIALS complete for facebook, instagram, and threads because PLATFORM_CREDENTIALS_FROM_ENV maps all three to this dict. Those connectors then appear configured and their Facebook/Threads/Graph OAuth flows use the Instagram Login App ID/secret, which the README documents as different from the Facebook App ID/secret, so direct-only deployments get broken connect options instead of leaving those platforms unconfigured.

Useful? React with 👍 / 👎.

}
_GOOGLE_CREDENTIALS = {
"client_id": env("PLATFORM_GOOGLE_CLIENT_ID", default=""),
Expand Down
24 changes: 24 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
app = 'brightbean-pnc'
primary_region = 'lax'

[build]

[env]
PORT = '8000'

[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a Fly worker process for background tasks

For deployments using this new fly.toml, only the default app process is started, so the image runs the Dockerfile Gunicorn CMD but never starts python manage.py process_tasks like Procfile:3 and the production compose worker do. The scheduled publisher, inbox sync, notification retries, and other recurring jobs in this repo are registered for django-background-tasks, so on Fly scheduled posts and background maintenance will not run unless a worker process group is defined and scaled alongside the web process.

Useful? React with 👍 / 👎.


[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1
processes = ['app']

[deploy]
release_command = 'python manage.py migrate --noinput'
11 changes: 7 additions & 4 deletions providers/instagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,14 @@ def supported_media_types(self) -> list[MediaType]:

@property
def required_scopes(self) -> list[str]:
# Meta renamed Instagram permissions with an "instagram_business_"
# prefix; the old names (instagram_basic, instagram_content_publish,
# etc.) are now rejected as invalid scopes.
return [
"instagram_basic",
"instagram_content_publish",
"instagram_manage_comments",
"instagram_manage_insights",
"instagram_business_basic",
"instagram_business_content_publish",
"instagram_business_manage_comments",
"instagram_business_manage_insights",
"pages_show_list",
"pages_read_engagement",
]
Expand Down
124 changes: 124 additions & 0 deletions scripts/connect_social_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Connect social accounts to BrightBean from stored credentials.

Wires Bluesky (handle + app password) and Mastodon (instance + user token)
into the BrightBean production workspace, replicating the web connect flows
(apps/social_accounts/views.py) for headless use.

Reads credentials from environment (set them as Fly secrets or pass inline):
BLUESKY_HANDLE, BLUESKY_APP_PASSWORD
MASTODON_INSTANCE_URL, MASTODON_ACCESS_TOKEN

Usage (production):
fly ssh console --app brightbean-pnc -C "python scripts/connect_social_accounts.py"

Does NOT post anything. Connecting only; publishing stays behind the Eric gate.
"""

import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")

import django

django.setup()

from django.utils import timezone # noqa: E402
from apps.social_accounts.models import SocialAccount # noqa: E402
from apps.workspaces.models import Workspace # noqa: E402
from apps.calendar.services import create_default_queue_and_slots # noqa: E402
from apps.credentials.models import PlatformCredential # noqa: E402
from providers.bluesky import BlueskyProvider # noqa: E402


def _get_workspace():
ws = Workspace.objects.order_by("created_at").first()
if ws is None:
raise RuntimeError("No workspace found in this BrightBean instance")
return ws


def _upsert(workspace, platform, *, platform_id, name, handle, avatar, followers,
access_token, refresh_token="", instance_url=""):
account, created = SocialAccount.objects.update_or_create(
workspace_id=workspace.id,
platform=platform,
account_platform_id=platform_id,
defaults={
"account_name": name,
"account_handle": handle or "",
"avatar_url": avatar or "",
"follower_count": followers or 0,
"oauth_access_token": access_token,
"oauth_refresh_token": refresh_token or "",
"instance_url": instance_url or "",
"connection_status": SocialAccount.ConnectionStatus.CONNECTED,
"last_error": "",
"analytics_needs_reconnect": False,
},
)
if created:
create_default_queue_and_slots(account)
return account, created


def connect_bluesky(workspace):
handle = os.environ.get("BLUESKY_HANDLE", "").lstrip("@")
app_password = os.environ.get("BLUESKY_APP_PASSWORD", "")
if not handle or not app_password:
print("Bluesky: SKIP (BLUESKY_HANDLE / BLUESKY_APP_PASSWORD not set)")
return
provider = BlueskyProvider()
tokens = provider.create_session(handle, app_password)
profile = provider.get_profile(tokens.access_token)
account, created = _upsert(
workspace, PlatformCredential.Platform.BLUESKY,
platform_id=profile.platform_id, name=profile.name, handle=profile.handle,
avatar=profile.avatar_url, followers=profile.follower_count,
access_token=tokens.access_token, refresh_token=tokens.refresh_token,
instance_url=provider.pds_url,
)
print(f"Bluesky: {'created' if created else 'updated'} -> {profile.handle}")


def connect_mastodon(workspace):
import requests

instance = os.environ.get("MASTODON_INSTANCE_URL", "").rstrip("/")
token = os.environ.get("MASTODON_ACCESS_TOKEN", "")
if not instance or not token:
print("Mastodon: SKIP (MASTODON_INSTANCE_URL / MASTODON_ACCESS_TOKEN not set)")
return
resp = requests.get(
f"{instance}/api/v1/accounts/verify_credentials",
headers={"Authorization": f"Bearer {token}"}, timeout=20,
)
if resp.status_code != 200:
print(f"Mastodon: token check failed ({resp.status_code}): {resp.text[:200]}")
print("Mastodon: if 403 'confirmed e-mail', click the confirmation link in the inbox first")
return
me = resp.json()
account, created = _upsert(
workspace, PlatformCredential.Platform.MASTODON,
platform_id=str(me["id"]), name=me.get("display_name") or me.get("acct", ""),
handle=me.get("acct", ""), avatar=me.get("avatar", ""),
followers=me.get("followers_count", 0),
access_token=token, instance_url=instance,
)
print(f"Mastodon: {'created' if created else 'updated'} -> {me.get('acct')}")


def main():
workspace = _get_workspace()
print(f"Workspace: {workspace.name} ({workspace.id})")
connect_bluesky(workspace)
connect_mastodon(workspace)
total = SocialAccount.objects.filter(workspace=workspace).count()
print(f"Connected social accounts in workspace: {total}")


if __name__ == "__main__":
main()