promote: fix false "posted" status + missing View-post links (reconcile browser posts) - #103
Merged
Merged
Conversation
…post links
Cookie-auth platforms (reddit, instagram, mastodon, x, linkedin, facebook,
threads) post asynchronously: postViaAccount only *enqueues* an sp_post and
returns { ok:true, webUrl:"" } before Playwright actually publishes. The sweep
took that as success and wrote the promo_post as `posted` with a null post_url —
so the "View post" link was hidden and a post that later failed (dead cookies /
login wall) still read as "posted". That's the reported reddit false-positive.
Fix: record cookie posts as `pending` linked to their sp_post, and reconcile the
real outcome from the worker once the browser post settles.
- migration: promo_post.sp_post_id → sp_post (link for reconciliation)
- lib/sp/post.ts: PostOk.pending flag; cookie branch returns pending:true
- lib/promote/sweep.ts: pending-aware insert (status/url/posted_at/sp_post_id),
pending counter; only sync API posts are `posted` immediately
- lib/promote/reconcilePromo.ts: on publish → set post_url + posted; on failure
→ refund the credit + mark failed (idempotent; no-op for non-promo posts)
- lib/sp/browserPost.ts: call reconcilePromo in the success + fail paths
- app/(app)/promote/[id]/page.tsx: show pending as "posting…"
- worker log + PromoteSweepResult: track pending
- tests: reconcilePromo (posted/failed-refund/idempotent/non-promo)
Note: existing historical rows already mislabeled `posted` with null URL aren't
retroactively reconciled (no sp_post link existed); this fixes it going forward.
Migration must be applied to prod by hand (psql over pooler) per the divergence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ralyodio
added a commit
that referenced
this pull request
Jul 17, 2026
…+ dead-session detection) (#105) * promote: repair browser-post selectors (reddit/facebook/threads/instagram) Best-effort fixes for the browser-automation posting failures now surfacing correctly after the #103 reconciliation fix. Each targets the exact element the Playwright error resolved to: - instagram: file input is display:none — wait for state:'attached', not 'visible' (a hidden input never becomes visible → the reported timeout). - reddit: scope the lexical body editor to :visible — a hidden editor template otherwise won .first() and never became visible. - facebook: drop the `[aria-label*="create"]` fallback (it substring-matched "…why you created moshcoding" on a hidden element); match the composer by button text instead; scope the editor to :visible. - threads: broaden the compose trigger (labels + "What's new?") and scope the editor to :visible. CAVEAT: these are UNVERIFIED — browser automation of live social sites can't be tested without the account's cookie session + a browser, and the sites change their DOM frequently. The durable fix is connecting reddit/facebook/threads via OAuth (the API adapters in lib/sp/post.ts already post reliably and return a real URL); Instagram has no post API so it stays browser-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * promote: evidence-based browser-post fixes from captured failure DOM Pulled the real page HTML/URL from the failed sp_post rows (debug_html/url) and fixed against the actual DOM instead of guessing: - reddit: the real body editor is inside <shreddit-composer name="body">; Reddit ALSO renders a hidden duplicate data-lexical-editor in the flair-picker modal (<r-post-flair-edit-modal>, slot="editor") which .first() kept grabbing → the "resolved to hidden" timeout. Scope to the body composer + :visible. - assertLoggedIn: wait for networkidle before judging (Instagram/Threads redirect to login CLIENT-SIDE after domcontentloaded, so the old immediate URL check missed dead sessions) and flag a near-empty shell as a login wall. Dead sessions now fail with SESSION_EXPIRED → account marked token_expired → the UI prompts a cookie reconnect instead of an opaque selector timeout. - captureDom: capture <body> outerHTML, not the whole document — FB's multi-MB <head> was eating the 600k budget and truncating before the composer, making FB failures undiagnosable. Diagnosis from the captured DOM (NOT code-fixable — user action): - instagram cookies are DEAD (redirects to /accounts/emailsignup) → re-export. - threads session dead/blocked (served a ~40-char shell) → re-export. - facebook IS logged in ("(3) Facebook") but its composer was past the old truncation; the wider capture + #105 selector tweaks should surface next run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ralyodio
added a commit
that referenced
this pull request
Jul 30, 2026
…gram) Best-effort fixes for the browser-automation posting failures now surfacing correctly after the #103 reconciliation fix. Each targets the exact element the Playwright error resolved to: - instagram: file input is display:none — wait for state:'attached', not 'visible' (a hidden input never becomes visible → the reported timeout). - reddit: scope the lexical body editor to :visible — a hidden editor template otherwise won .first() and never became visible. - facebook: drop the `[aria-label*="create"]` fallback (it substring-matched "…why you created moshcoding" on a hidden element); match the composer by button text instead; scope the editor to :visible. - threads: broaden the compose trigger (labels + "What's new?") and scope the editor to :visible. CAVEAT: these are UNVERIFIED — browser automation of live social sites can't be tested without the account's cookie session + a browser, and the sites change their DOM frequently. The durable fix is connecting reddit/facebook/threads via OAuth (the API adapters in lib/sp/post.ts already post reliably and return a real URL); Instagram has no post API so it stays browser-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug (reported)
On the Promote page, reddit / instagram / mastodon (and other cookie-auth platforms) show no "View post" link, and posts read as posted even though they never landed in the account (e.g. no matching post in Reddit history).
Root cause
Cookie-auth platforms post asynchronously.
postViaAccount(lib/sp/post.ts) only enqueues ansp_postfor the Playwright worker and returns{ ok:true, webUrl:"" }before publishing. The sweep (lib/promote/sweep.ts) treated that as success and wrote thepromo_postasstatus='posted'withpost_url=null:post_urlis set.postedat enqueue time; if the async browser post later fails (dead cookies / login wall / image-gen failure) nothing ever reconciled it back, so it stays green with no link.API-based platforms (bluesky/telegram/discord + OAuth reddit/mastodon) post synchronously and return a real URL — which is why those work.
Fix — reconcile the async outcome
Mirror the existing
reconcileOutreachpattern:promo_post.sp_post_id→sp_postlinks the promo row to its queued browser post.sweep.tsrecords cookie posts aspending(null url, noposted_at) with thesp_post_id; only synchronous posts arepostedimmediately. Adds apendingcounter.post.ts—PostOk.pendingflag; the cookie branch returnspending:true.reconcilePromo.ts(new) — when the worker publishes, set the realpost_url+posted; when it fails, refund the credit and markfailed. Idempotent; no-op for non-Promote posts.browserPost.tscallsreconcilePromoin both the success and failure paths.reconcilePromo(posted / failed-refund / idempotent / non-promo).Result
Cookie posts now show posting… → posted (with a real View-post link) or failed (with the error + credit refunded) based on what actually happened — no more phantom "posted".
Per the runbook, prod migration history has diverged — apply
supabase/migrations/20260717140000_promo_post_sp_link.sqlviapsqlover the pooler after merge (nullable column + index, safe).Not covered
Historical rows already mislabeled
postedwith a null URL aren't retroactively reconciled (nosp_postlink existed for them) — this fixes it going forward.Verified
tsc(app + worker) clean ·next buildcompiles · reconcilePromo tests 4/4 · generate-pitch tests still green.🤖 Generated with Claude Code