{"slug":"marketing-command-center","total":186,"limit":100,"offset":0,"since":null,"commits":[{"hash":"39fcdef","date":"2026-07-27 15:56:08 -0700","author":"Steve Abrams","subject":"MCC bluesky: fail loud if image can't embed — never silently post text-only","body":"postBluesky() previously swallowed any image fetch/uploadBlob failure and posted\ntext-only anyway (embed left undefined), which slips past the route-level text-only\nguard (content.mediaUrl was truthy upstream) and violates Steve's 2026-07-17\n'DW never posts text-only' rule — silently, on ok:true. Now aborts the post with a\nloud error on any fetch/upload failure. Found by /contrarian gating TK-8.\n\nCo-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"},{"hash":"f4429c8","date":"2026-07-27 15:21:42 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-27T15:21:37 (1 files) — data/cc-warmup-seed-200.json","body":""},{"hash":"a9436a5","date":"2026-07-27 14:51:31 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-27T14:51:27 (1 files) — data/cc-import-activities-audit.jsonl","body":""},{"hash":"d530fe4","date":"2026-07-27 14:21:19 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-27T14:21:14 (2 files) — data/cc-import-fm-clients-hold.json data/cc-import-fm-clients-mailable.json","body":""},{"hash":"747d83a","date":"2026-07-26 04:40:39 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-26T04:40:35 (1 files) — public/data/clients-manifest.json","body":""},{"hash":"38e14e7","date":"2026-07-22 12:44:15 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-22T12:44:11 (1 files) — package-lock.json","body":""},{"hash":"85f3566","date":"2026-07-22 12:36:18 -0700","author":"Steve Abrams","subject":"chore: version bump (session close)","body":""},{"hash":"5d1c41b","date":"2026-07-22 12:20:02 -0700","author":"Steve Abrams","subject":"chore(mcc): move IG re-pull schedule from Mac2 launchd to Kamatera-local cron","body":"The re-pull's script AND data both live on Kamatera, so a Mac2 launchd job SSHing\nin was a needless cross-machine dependency (Mac2 asleep / SSH hiccup at 06:20 would\nsilently skip it — the exact drift the job exists to prevent). Now a self-contained\nroot crontab entry on Kamatera (06:40 daily). Removed the Mac2 wrapper + plist;\nadded scripts/ig-repull-SCHEDULE.md documenting the cron. Verified: cron present,\na manual Kamatera run wrote a fresh healthy status, Mac2 job booted out + gone.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"6b2db25","date":"2026-07-22 12:14:02 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-22T12:13:58 (1 files) — data/ig-repull-status.json","body":""},{"hash":"21be5cc","date":"2026-07-22 12:13:05 -0700","author":"Steve Abrams","subject":"feat(mcc): board shows IG re-pull health at a glance + Run-now fix button","body":"The daily IG media re-pull (com.steve.mcc-ig-repull) had no surface — you couldn't\nsee if it was running or stale. Now the Streams Board shows a health strip above\nthe columns: green/amber/red dot + last-run time + result, and a \"Run now\" button.\n\n- ig-media-repull.js: writes data/ig-repull-status.json every run (ranAt/mode/\n  recovered/graphErr/downloadDead/unrecoverable/ok).\n- channels: GET /api/channels/ig-repull-status (reads it) + POST /api/channels/\n  ig-repull-run (triggers --apply as a guarded child; 409 if already running).\n- board.js: #bd-repull-health strip — amber if never-run or stale (>36h), red on\n  graph/download errors, green when clean; \"Run now\" POSTs then refreshes.\n\nVerified locally: status write, GET, POST trigger, concurrent-run 409 guard, and\nthe strip renders (green dot, timestamp, Run-now button).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"6d2f55b","date":"2026-07-22 11:25:17 -0700","author":"Steve Abrams","subject":"chore(mcc): daily launchd job for the IG media re-pull (Mac2 → Kamatera SSH)","body":"scripts/ig-repull-cron.sh: wrapper the LaunchAgent runs — SSHes to Kamatera and\nruns ig-media-repull.js --apply, logging to ~/.claude/logs/mcc-ig-repull.log.\nscripts/com.steve.mcc-ig-repull.plist: reference copy of the installed LaunchAgent\n(runs daily 06:20; the live copy lives in ~/Library/LaunchAgents). Matches the DW\nscheduled-ops pattern (launchd on Mac2 SSHing to Kamatera, like the canaries).\nBootstrapped + kick-tested end-to-end: SSH → --apply → exit 0.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"214b1bb","date":"2026-07-22 11:05:17 -0700","author":"Steve Abrams","subject":"fix(mcc): IG re-pull only touches at-risk images, never clobbers stable URLs","body":"The re-pull was over-eager — it would refresh a published-IG post even when its\nmediaUrl is already a durable cdn.shopify.com image, downgrading a full-res photo\nto a square IG thumbnail. Now it only targets items whose current mediaUrl is\nmissing or an expiring ig/fbcdn signed URL; stable non-IG hosts are left alone.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"971082a","date":"2026-07-22 11:03:03 -0700","author":"Steve Abrams","subject":"feat(mcc): IG Graph API re-pull to restore durable thumbnails for published IG posts","body":"scripts/ig-media-repull.js: for every outbox item successfully published to\nInstagram (detail[].id = media id, detail[].igId = business account), fetch the\nmedia object's CURRENT media_url/thumbnail_url from Graph v23.0 — a fresh live URL\n— and cache the bytes locally keyed by the STABLE media id, then rewrite the\nitem's mediaUrl to the /cache/media path. Makes published-IG thumbnails durable\nregardless of future signed-URL rotation. Token resolved per-igId from\nmeta-pages.json (per-Page token), falling back to META_ACCESS_TOKEN/IG_ACCESS_TOKEN.\n\nDry-run by default (read-only Graph calls + report); --apply caches + rewrites the\noutbox after backing it up. Items with a dead IG url but NO media id (failed\ncross-posts) are reported unrecoverable — the placeholder tile is correct for those.\n\nlib/media-cache.js: factor out fetchImageBuffer + writeAtomic; add exported\ncacheAs(url, keyBase) for explicit-key (media-id) caching used by the re-pull.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"2ffb36f","date":"2026-07-22 10:42:05 -0700","author":"Steve Abrams","subject":"fix(mcc): durable local cache for expiring IG/fbcdn post images + graceful placeholder","body":"Board post thumbnails embed raw Instagram/fbcdn CDN URLs whose _nc_ohc/oh/oe\nsignature is time-boxed; once it expires the URL 403s for everyone (verified:\neven a bare server-side fetch 403s), so the <img> failed and the old\nonerror hid it -> blank tile (\"posts all blank\").\n\n- lib/media-cache.js: localize() maps an IG/fbcdn URL to /cache/media/<sha1(path)>.<ext>,\n  serving a cached local copy if present and background-downloading (best-effort,\n  6s timeout, SSRF host-allowlist, atomic write) any missing one so the next load\n  self-heals. Cache key is the media PATH (not the rotating signed query) so one\n  media = one file. Never blocks the response.\n- channels /api/outbox: localizeItems() the outbox before serving.\n- board.js: on img error, swap to an inline branded \"image unavailable\" SVG tile\n  instead of hiding — a dead/uncached image now reads as intentional, not blank.\n- .gitignore: public/cache/ (locally-cached media, not source).\n\nVerified end-to-end: outbox GET returns the localized /cache/media path and the\nfile serves 200; download pipeline writes valid image bytes. Deploy held (gated).\nNote: images whose URL already expired before caching can't be recovered here —\nfully restoring those needs a re-pull of fresh media from the IG Graph API (follow-up).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"243d9c5","date":"2026-07-22 10:31:51 -0700","author":"Steve Abrams","subject":"fix(mcc): guard panel-router race that blanked panels + untrack screen-record videos","body":"route() in public/app.js had no concurrency guard across its two awaits (fetch\npanel HTML, load panel JS), so rapid panel switching interleaved two route()\ncalls that both wrote the shared #panel — a stale one landed after the fresh\nrender and wiped it (title stays, zero cards/API/errors = \"posts all blank\").\nAdded a latest-wins token: after each await, bail if a newer nav superseded.\nVerified via /screenrecord (5-run order-varied recording, run3 reproduced the\nall-blank cascade) + a fixed-app.js-over-live-data proxy A/B. Deploy held (gated).\n\nAlso .gitignore screenrecord/rec/ + *.webm and untrack the 10 committed webm\nrecordings (7.5MB) an auto-save hook had pulled into history going forward.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"5a92b90","date":"2026-07-22 10:13:14 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-22T10:13:11 (5 files) — scripts/repro-run3.js screenrecord/DEBUG-REPORT.md scripts/probe-403.js scripts/verify-fix.js scripts/verify-fix2.js","body":""},{"hash":"c073e61","date":"2026-07-22 09:43:05 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-22T09:43:01 (8 files) — public/app.js screenrecord/ scripts/nav-map.js scripts/probe-403-and-posts.js scripts/repro-race.js","body":""},{"hash":"355b730","date":"2026-07-22 08:42:53 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-22T08:42:49 (1 files) — package-lock.json","body":""},{"hash":"0eff96e","date":"2026-07-22 08:12:51 -0700","author":"Steve Abrams","subject":"chore: v1.7.0 (session close — Threads un-gate, image+SKU generation, honest postable status, /publish skip)","body":""},{"hash":"29d1ff2","date":"2026-07-22 07:50:51 -0700","author":"Steve Abrams","subject":"channels: /publish skips non-postable + media-mismatched channels","body":"A full-fan-out publish used to FIRE every connected channel and collect\nguaranteed errors (FB App-Review #200, LinkedIn 401, TikTok/YouTube image-as-\nvideo). Now /publish consults the postable signal + media-type fit and SKIPS\n(stages with a clear reason) any channel that can't actually take this post,\nfiring only the ones that will land. Result rows carry skipped:true + reason.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"69af9fe","date":"2026-07-22 07:33:09 -0700","author":"Steve Abrams","subject":"channels: add honest 'postable' status — connected != can-actually-post","body":"The status endpoint reported connected:true for channels that cannot post\n(Facebook needs pages_manage_posts App Review; LinkedIn token is unscoped/dead;\nTikTok/YouTube are video-only), which misled callers twice. Added a POSTABILITY\nmap encoding the known structural blocks and surface per channel:\n  postable     — connected AND not structurally blocked (the real signal)\n  mediaTypes   — what it accepts (image caller must check; TikTok/YouTube = video-only)\n  postableNote — the why (App Review / dead token / video-only)\nAlso corrected the stale Threads caveat (tester-mode posts live without App Review).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"d84dc2a","date":"2026-07-22 06:57:11 -0700","author":"Steve Abrams","subject":"engine: untrack runtime engine-queue.json + protect it on deploy","body":"engine-queue.json holds live approve/arm/post status mutated only by the running\nMCC — it was git-tracked (a latent trap: every deploy/commit carried a stale\nsnapshot). Untracked it + gitignored (its sibling engine-config.json already is),\nand added it + engine-config.json to .deploy.conf RSYNC_EXTRA_EXCLUDES so a\n--delete rsync can never clobber the live queue.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"d9e22d3","date":"2026-07-22 06:50:29 -0700","author":"Steve Abrams","subject":"engine: image-grounded captions (vision) + always include DW SKU","body":"Two changes to daily post-suggestion generation:\n\n1. VISION-GROUNDED COPY — captionsFor() now runs a vision pass (qwen2.5vl:7b)\n   on the actual product image first, producing a factual visual brief (motif,\n   scale, color, texture) that grounds the qwen3:14b copy. Old captions were\n   text-only from title/tags and read generic; new ones describe what the image\n   actually shows. Graceful: image fetch / vision failure (incl. non-Mac hosts\n   that can't reach ollama) resolves to '' and captions fall back to text-only.\n   One vision call per product (not per channel). Source label gains '+vision'.\n\n2. ALWAYS INCLUDE THE DW SKU — candidates.resolveSku() fills dwSku from\n   dw_sku || sku || variant_sku (strips the '-Sample' suffix; dw_sku is often\n   empty on the mirror). llm.withSku() then guarantees the SKU in every caption,\n   sized so channel limits never truncate it, and strips any dangling '· SKU'\n   tail when no code exists. SYSTEM/userPrompt updated to request it (belt) with\n   withSku as the deterministic backstop (suspenders).\n\nVerified end-to-end on the real Onda image: vision read the wave motif, all 7\nchannels carried '· SKU DWTT-73419' within limits; empty-SKU + broken-image\nedge cases degrade cleanly. $0 (local ollama).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"7ff5d7c","date":"2026-07-22 06:34:12 -0700","author":"Steve Abrams","subject":"deploy: ship Threads un-gate to Kamatera MCC (app 69 reloaded, health 200)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"2545273","date":"2026-07-22 06:30:04 -0700","author":"Steve Abrams","subject":"engine: stop birth-gating Threads suggestions (DW is a Meta tester, publishes without App Review)","body":"Threads posts live in tester mode (proven: postThreads() succeeded this batch),\nbut generate.js was stamping every new Threads item status:gated from the static\nGATED_CHANNELS map. Removed threads from that map so items are born 'suggested'\nand approvable; the per-post approve->schedule step is still the real publish gate.\nTikTok stays gated (genuinely SELF_ONLY until app audit).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"5459b68","date":"2026-07-21 17:54:20 -0700","author":"Steve Abrams","subject":"LinkedIn: filed Developer Support ticket CAS-11523819 to unblock CMA (auto-added defaults on app 257140099)","body":""},{"hash":"aae7c9c","date":"2026-07-21 17:19:30 -0700","author":"Steve Abrams","subject":"LinkedIn: record CMA self-serve blocker (auto-added default products grey Request access; app 257140099 created+verified, client 86z1h5ic86lw65, org 3090253)","body":""},{"hash":"2becbe5","date":"2026-07-21 16:33:12 -0700","author":"Steve","subject":"submissions: Facebook pages_manage_posts App Review package (dashboard steps + use-case + screencast script + reviewer repro)","body":""},{"hash":"eb16f00","date":"2026-07-21 16:33:09 -0700","author":"Steve","subject":"submissions: LinkedIn dedicated-app + Community Management API request package (new-app spec + email-verify + post-approval finish)","body":""},{"hash":"4e9c425","date":"2026-07-21 16:32:17 -0700","author":"Steve","subject":"submissions: TikTok production App Review package (fields + scope text + demo shot-list + submit steps)","body":""},{"hash":"33d5174","date":"2026-07-21 15:47:51 -0700","author":"Steve Abrams","subject":"chore: v1.6.3 (session close — FB page names fix)","body":""},{"hash":"3779b0f","date":"2026-07-21 15:45:01 -0700","author":"Steve","subject":"engine: add LinkedIn as 7th channel — image-upload adapter (initializeUpload→PUT→post), status entry, trade-voice captions, channel backfill for already-generated days","body":""},{"hash":"71a303c","date":"2026-07-21 15:41:15 -0700","author":"Steve","subject":"5x: Engine panel verified clean on live marketing.dw (0 app defects; 2 harness fixes, 1 phantom discarded)","body":""},{"hash":"024a26b","date":"2026-07-21 15:28:00 -0700","author":"Steve Abrams","subject":"channels: show real Facebook Page names instead of raw IDs in status panel","body":""},{"hash":"0210a80","date":"2026-07-21 15:02:09 -0700","author":"Steve","subject":"engine: contrarian fixes — YT approve defaults private, edit routed through CAS (double-post guard), TZ pinned PT, productUrl in outbound content (+bluesky budget), LLM claim-grounding","body":""},{"hash":"0b73831","date":"2026-07-21 14:52:46 -0700","author":"Steve","subject":"engine: brain (candidates+llm+generate), scheduler (CAS tick+crash sweep), Engine panel UI","body":""},{"hash":"0938d51","date":"2026-07-21 14:36:07 -0700","author":"Steve","subject":"engine: core module (store/config/skeleton) + channels.postLive + leak-deny lib","body":""},{"hash":"e8501e8","date":"2026-07-21 14:36:04 -0700","author":"Steve","subject":"assets: local qwen2.5vl vision enrichment (aiTags) + search","body":""},{"hash":"70479a4","date":"2026-07-21 14:13:19 -0700","author":"Steve","subject":"youtube: add force-ssl scope for videos.update (visibility control)","body":""},{"hash":"e10410d","date":"2026-07-21 14:09:37 -0700","author":"Steve","subject":"youtube: add /youtube/visibility endpoint (flip private↔public via videos.update)","body":""},{"hash":"85d08d7","date":"2026-07-21 13:43:43 -0700","author":"Steve","subject":"youtube: build resumable videos.insert upload pipeline (postYouTube) + token refresh","body":""},{"hash":"ed9e2e2","date":"2026-07-21 13:33:09 -0700","author":"Steve","subject":"channels: derive TikTok + YouTube account handles from env (accounts rail was blank for both)","body":""},{"hash":"ca60a5c","date":"2026-07-21 12:45:27 -0700","author":"Steve","subject":"chore: v-bump (session close — TikTok token-mirror fix)","body":""},{"hash":"34ea50f","date":"2026-07-21 12:27:23 -0700","author":"Steve","subject":"TikTok App Review: mark posting pipeline VERIFIED (creator_info ok); demo needs only a hosted video + gated test-post","body":""},{"hash":"a9c34db","date":"2026-07-21 12:21:41 -0700","author":"Steve","subject":"Add TikTok App Review submission package (production fields + demo script + posting-pipeline prereq)","body":""},{"hash":"63d0cd0","date":"2026-07-20 16:31:19 -0700","author":"Steve","subject":"TikTok: mirror OAuth token into env on callback (fixes Bearer undefined on post) + demo recorder scaffold","body":""},{"hash":"9a142fd","date":"2026-07-20 15:02:51 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-20T15:02:48 (1 files) — package-lock.json","body":""},{"hash":"76de953","date":"2026-07-20 14:40:15 -0700","author":"Steve","subject":"chore: v1.6.1 (session close — Schumacher calendar filter)","body":""},{"hash":"5ea8d24","date":"2026-07-20 14:07:46 -0700","author":"Steve Abrams","subject":"calendars: filter Schumacher (internal-only) out of both calendar layers at source","body":"Schumacher is internal-only and must never surface on a front end. The All\nCalendars feed re-projects from dw_unified every ~10min, so scrubbing the JSON\nalone would be undone on the next regen. Added a HIDDEN_VENDORS source filter\napplied to the activation layer (after date projection, so other vendors keep\ntheir real go-live slots), the launched layer, and the snapshot-fallback path.\nVerified: live regen yields 0 Schumacher (activation 20304 -> 20034).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"540ae08","date":"2026-07-20 13:21:47 -0700","author":"Steve","subject":"chore: lint + refactor + v1.6.0 (session close — reel/FB-page picker hardening)","body":""},{"hash":"97dcfe7","date":"2026-07-20 12:56:36 -0700","author":"Steve","subject":"Composer: add reel/story asset picker + Facebook Page selector","body":"- /api/composer/reels serves generated reels as selectable video post media\n- Reels tab (portrait video tiles) → sets videoUrl; video preview in composer\n- Facebook Page picker (reuses /api/channels/pages) shown when FB is targeted\n- postFacebook/postInstagram now publish video (FB /videos, IG REELS) + selected pages\n- publish body carries videoUrl + pages; verified headless (25 reels, 80 pages, 0 errors)"},{"hash":"02a76e0","date":"2026-07-19 07:55:27 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-19T07:55:24 (1 files) — package-lock.json","body":""},{"hash":"947d85f","date":"2026-07-19 07:28:43 -0700","author":"steve","subject":"chore: v1.5.0 (session close — Clients & Prospects panel, Threads accounts, gated CC import staging)","body":""},{"hash":"861cc69","date":"2026-07-19 04:54:48 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-19T04:54:45 (1 files) — public/data/clients-manifest.json","body":""},{"hash":"d77d6ee","date":"2026-07-17 10:45:51 -0700","author":"Steve Abrams","subject":"chore: lint, refactor (drop dead env()), v1.4.0 (session close)","body":""},{"hash":"c1d7a1d","date":"2026-07-17 09:58:38 -0700","author":"Steve Abrams","subject":"instagram: poll media container status until FINISHED before publish — fixes 'Media ID is not available' (Meta processes images async)","body":""},{"hash":"47459ec","date":"2026-07-17 09:48:40 -0700","author":"steve","subject":"cc-import: stage gated FM-client → Constant Contact import (34,825 contacts; scraped list excluded); upload drafted to pending-approval (DTD verdict A)","body":""},{"hash":"c333624","date":"2026-07-17 09:46:08 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-17T09:46:05 (2 files) — modules/channels/index.js public/panels/composer.js","body":""},{"hash":"19aea36","date":"2026-07-17 09:24:36 -0700","author":"Steve Abrams","subject":"bluesky: rich-text FACETS — #hashtags + links now clickable (UTF-8 byte-offset aware per docs.bsky.app)","body":""},{"hash":"c16d138","date":"2026-07-17 09:16:46 -0700","author":"Steve Abrams","subject":"Vendor IG: mark BN Walls 'none found' (no Business-Discovery-readable IG); drop orphan cache entry","body":""},{"hash":"3c65730","date":"2026-07-17 09:16:03 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-17T09:16:00 (1 files) — package-lock.json","body":""},{"hash":"01590c1","date":"2026-07-17 09:13:22 -0700","author":"Steve","subject":"chore: lint (with-open autofix), refactor (none needed), v1.3.0 → v1.3.1 (session close)","body":""},{"hash":"959a2f0","date":"2026-07-17 09:03:53 -0700","author":"Steve Abrams","subject":"Vendor IG Reporting: wire never-expiring page token, page-token discovery, standalone refresh CLI; populate 33/34 vendors","body":""},{"hash":"b69f993","date":"2026-07-17 08:56:37 -0700","author":"Steve","subject":"MCC Clients: server-side notes store so notes sync across devices","body":"- /api/clients-notes GET+POST in server.js (atomic JSON store at\n  data/clients-notes.json), mounted directly (not via registry) so no phantom\n  nav panel; behind the existing basic-auth\n- clients.js: pull+merge server notes on group open (server wins, local-only\n  notes pushed up), debounced POST on each edit; localStorage stays the instant\n  cache. Textarea hints 'syncs across devices'\n- protect data/clients-notes.json from rsync --delete on deploy (.deploy.conf\n  RSYNC_EXTRA_EXCLUDES + .gitignore)"},{"hash":"8738173","date":"2026-07-17 08:56:29 -0700","author":"Steve Abrams","subject":"deploy: exclude live-authoritative runtime data (outbox/tokens/pages/assets) from rsync — deploys were clobbering the live Meta token, staged posts, and UI-added assets","body":""},{"hash":"2888aa4","date":"2026-07-17 08:55:50 -0700","author":"Steve Abrams","subject":"board UX: add 'Clear staged' — dismisses stale staged/pending/failed cards (keeps posted history) via new POST /api/channels/outbox/clear; Compose links to Composer","body":""},{"hash":"075ec0c","date":"2026-07-17 08:54:50 -0700","author":"Steve Abrams","subject":"Vendor IG Reporting: render last 3 posts inline per vendor (was 10-photo grid)","body":""},{"hash":"922c32f","date":"2026-07-17 08:48:11 -0700","author":"steve","subject":"clients panel: chip clicks open target in a right-side drawer (website embeds live; LinkedIn/IG show one-click card since they block framing)","body":""},{"hash":"2aac093","date":"2026-07-17 08:45:25 -0700","author":"Steve","subject":"MCC Clients: per-contact notes field below each firm card","body":"- auto-saving, auto-growing textarea under each broker/listing card, persisted\n  per group in localStorage (keyed by the same recId as contacted/actions)\n- gold 'has-note' highlight when a note exists; notes included in CSV export"},{"hash":"30afe18","date":"2026-07-17 08:29:17 -0700","author":"Steve","subject":"MCC Clients: show full address + phone + real domain + best contact/follow per firm","body":"- backfill address/state/category/mapsUrl from the source Google-Places CSVs\n  into the staged prospect JSONs (scripts/backfill-address-from-csv.py), with a\n  cross-CSV host fallback for the group whose source CSV is absent\n- clients.js: render 📍 full address (Google Maps link), 📞 tap-to-call phone\n  chip, replace generic 'site ↗' with the actual domain (bsbdesign.com ↗), and\n  a ⭐ Best contact/follow recommendation (gold-rings the recommended channel)\n- carry the richer location fields forward in crawl-csv-emails.py so future\n  re-crawls keep them; export CSV gains Address/State/Category/Best-contact cols"},{"hash":"7877a92","date":"2026-07-17 08:19:56 -0700","author":"Steve Abrams","subject":"composer: banner preview respects template aspect-ratio (#2) + warns when no asset selected (#3) — contrarian findings. Font-fail finding (#1) verified PHANTOM (allow-same-origin loads fonts), dropped.","body":""},{"hash":"01d65e1","date":"2026-07-17 08:14:18 -0700","author":"Steve Abrams","subject":"composer: Banners tab renders a LIVE banner preview (real /api/layouts/render HTML in the middle zone) instead of a dead deep-link","body":""},{"hash":"a4a92b1","date":"2026-07-17 08:06:17 -0700","author":"Steve Abrams","subject":"layouts: fix malformed placeholder SKUs — DW-GL-001 → DWGL-100001 (real DW format DW<code>-######)","body":""},{"hash":"4877837","date":"2026-07-17 07:58:46 -0700","author":"Steve Abrams","subject":"5x: contrarian gate resolved — generate+publish now functionally verified end-to-end","body":""},{"hash":"f3e5129","date":"2026-07-17 07:57:22 -0700","author":"Steve Abrams","subject":"composer: Generate copy now sends kind=instagram (was 'social' → silently degraded to headline). Contrarian catch.","body":""},{"hash":"27b2af2","date":"2026-07-17 07:50:46 -0700","author":"Steve Abrams","subject":"5x: composer clean-twice report","body":""},{"hash":"917acca","date":"2026-07-17 07:49:33 -0700","author":"Steve Abrams","subject":"5x sweep 4: add inline SVG favicon — kills the app-wide /favicon.ico 404 (last console error)","body":""},{"hash":"c5779ce","date":"2026-07-17 07:46:04 -0700","author":"Steve Abrams","subject":"5x sweep 2: composer drops broken asset thumbnails (dead URLs no longer 404-spam the console or show broken tiles)","body":""},{"hash":"8b2fd01","date":"2026-07-16 17:50:49 -0700","author":"Steve Abrams","subject":"composer: new central board — combine asset/banner + copy → publish to all connected socials at once","body":""},{"hash":"5e38c36","date":"2026-07-16 17:43:40 -0700","author":"steve","subject":"clients panel: one-click 'Copy not-actioned emails' (respects search+filters, skips already-actioned)","body":""},{"hash":"05fccc4","date":"2026-07-16 17:39:37 -0700","author":"steve","subject":"clients panel: 'not actioned' filter + LinkedIn/Instagram actioned timestamp columns in CSV export","body":""},{"hash":"88eb92d","date":"2026-07-16 17:35:19 -0700","author":"steve","subject":"clients panel: inline LinkedIn/Instagram Follow chips under each name + date-time stamp on Follow/message click (persisted per contact/channel)","body":""},{"hash":"bb5f304","date":"2026-07-16 17:15:28 -0700","author":"Steve Abrams","subject":"docs: IG fleet connect checklist (35 live + convert/link steps for the rest)","body":""},{"hash":"bec82b0","date":"2026-07-16 16:53:59 -0700","author":"Steve Abrams","subject":"channels: add /ig-audit — enumerate ALL Instagram accounts (page-linked + business owned/client), not just the 34 page-linked","body":""},{"hash":"a8a2166","date":"2026-07-16 16:43:49 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-16T16:43:46 (1 files) — package-lock.json","body":""},{"hash":"25362f4","date":"2026-07-16 16:40:42 -0700","author":"Steve","subject":"chore: lint, refactor, v1.3.0 (session close)","body":"Cosmetic-only cleanup of modules/calendars/lib.js (googleEvents stub params\nuncommented to match call site; split a lone double-declaration). No behavior\nchange — smoke test green (activation+launched live, counts intact). Minor bump\nreflects this session's launched-layer-live-on-Kamatera capability."},{"hash":"1473c68","date":"2026-07-16 12:13:15 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-16T12:13:12 (1 files) — public/data/clients-manifest.json","body":""},{"hash":"5723126","date":"2026-07-16 12:00:40 -0700","author":"steve","subject":"clients panel: coverage summary (contacts/emails/IG/LinkedIn across all groups); manifest carries per-group counts","body":""},{"hash":"b1d1fc7","date":"2026-07-16 11:59:29 -0700","author":"steve","subject":"clients panel: dedup + field-merge in All Prospects (LA groups overlap; keeps richest contact record)","body":""},{"hash":"d544ba1","date":"2026-07-16 11:58:51 -0700","author":"steve","subject":"clients panel: merged ★ All Prospects group (search/sort/filter/export across all ~5.9k firms; each tagged by city)","body":""},{"hash":"63885bc","date":"2026-07-16 11:57:39 -0700","author":"steve","subject":"clients panel: quick filters (email/IG/LinkedIn/uncontacted), Copy-all-emails, IG+LinkedIn count pills, density toggle (completes sort+density rule)","body":""},{"hash":"d885131","date":"2026-07-16 11:43:10 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-16T11:43:05 (1 files) — public/data/clients-manifest.json","body":""},{"hash":"1a67441","date":"2026-07-16 11:30:24 -0700","author":"steve","subject":"clients panel: search across ALL fields + sort dropdown (name/city/state/has-email/IG/LinkedIn), persisted","body":""},{"hash":"e0ae6f5","date":"2026-07-16 11:17:41 -0700","author":"steve","subject":"clients: browser+local-model contact enrichment (direct Instagram/LinkedIn + email gap-fill)","body":"- enrich-contacts-browser.mjs: headless Chromium (Playwright) renders JS-only\n  sites the HTTP crawler misses; pulls direct instagram/linkedin/tel from the\n  DOM + mailto; falls back to local Ollama (qwen2.5) to deobfuscate emails. $0.\n- Panel shows DIRECT IG/LinkedIn (gold ↗) when resolved, search (⌕) otherwise;\n  CSV export carries the direct links. Wired into refresh-clients.sh weekly."},{"hash":"ad37a60","date":"2026-07-16 11:06:44 -0700","author":"steve","subject":"refresh-clients.sh: read CSVs from project data/sources (launchd can't read ~/Downloads — TCC), nullglob rsync","body":""},{"hash":"87dc441","date":"2026-07-16 11:03:08 -0700","author":"Steve","subject":"calendars: decouple launched layer so it stays live where bulk_fivefield_worklist is absent (Kamatera)","body":"Give the worklist (RR_SQL) query its own inner try/catch inside computeActivation\nso a missing bulk_fivefield_worklist (Mac-only table) no longer sinks the launched\nlayer, which reads only shopify_products. When the worklist is absent, activation\nitems come from the last-good snapshot while launched recomputes LIVE. Adds\nactivation_live/launched_live flags; keeps top-level error null in that normal case\nso the 'Live catalog unavailable' banner stops mis-firing."},{"hash":"c1ee3c8","date":"2026-07-16 11:01:23 -0700","author":"steve","subject":"clients panel: per-group CSV export (respects filter; includes emails + LinkedIn/IG URLs; local download, not CC)","body":""},{"hash":"b50489a","date":"2026-07-16 11:00:16 -0700","author":"steve","subject":"refresh-clients.sh: mkdir lock (macOS has no flock)","body":""},{"hash":"09230f0","date":"2026-07-16 10:57:21 -0700","author":"steve","subject":"refresh-clients.sh: pin PATH for launchd (node/python/homebrew)","body":""},{"hash":"2ba021c","date":"2026-07-16 10:56:22 -0700","author":"steve","subject":"register clients module in registry so the panel mounts in the nav","body":""}]}