Skip to content

Successful yazar promote leaves divan roster and backlog stale #7036

Description

@usirin

Verified at origin/main (5777b03): every claim below was re-read in source by triage — the gap is live at main, not superseded.

A moderator promotes a çaylak from the divan; the server commits correctly and atomically. Pasaport.promoteToYazar (apps/web/worker/features/pasaport/Pasaport.ts) runs one D1 batch whose first statement is the guarded tier flip (the tier = 'çaylak' guard makes a repeat call return promoted: false) followed by sandbox sweeps over the definition/post/comment tables. The reviewer's screen never learns:

  • ReviewerActions.onPromote (apps/web/src/components/divan/CaylakDetail.tsx) awaits fate.mutations.user.promote(...), maps the outcome to a status string, and touches no read state — neither divan.roster nor the open divan.backlog.
  • Both lists are plain one-shot reads: DivanRoster.tsx issues useRequest for the gated divan.roster root, CaylakDetail.tsx for divan.backlog. The divan has no /fate/live topic by design — apps/web/worker/features/divan/queries.ts says so itself: "No flag, no /fate/live topic: a plain one-shot query."
  • The shared post-promote publish publishPromotion (apps/web/worker/features/pasaport/promote-live.ts) invalidates pano feed/thread/post/comment topics, sozluk definition topics, and the user entity only — nothing repairs either divan connection.

So the promoted person stays visible in the roster, their items stay pending, and a second press reports "kullanıcı zaten yazar." against stale state — reproduced on production today. Server truth is consistent (a fresh session shows the account gone from the roster), so this is purely client-side staleness. Triage note: the profile-page path has the same shape — PromotionActions.onPromote (apps/web/src/components/profile/PromotionActions.tsx) also sets only a message, and UserProfilePage.tsx reads the profile through cached useRequest rather than a live view — so its displayed çaylak/yazar status can go stale the same way.

Triage note: verified against react-fate 1.3.1 typings — useRequest returns a plain root map and its options carry only a fetch mode; there is no refetch handle. The original filer's guess stands: refreshing two private roots after a mutation may need a small fate-side expire/refetch primitive, or the handler can drive a fresh request itself. The mechanism is builder latitude; the observable outcome below is the contract.

Why this is standalone work

Searched the board for an owner of the post-promote client-refresh surface ("divan live subscription refetch stale cache after mutation", "promote yazar UI refresh moderator review") — no open issue or epic matches. Nearest neighbours do not own it: #1856 (moderation live pressure / divan surge badge) is an optional live-topic enhancement, not this staleness bug; #1206 (promotion feature) and #1841 (profile-button visibility) are closed. Minted standalone.

What done looks like

  • After a successful promote from the divan reviewer actions, the promoted çaylak disappears from the rendered roster and their pending items disappear from the rendered backlog without a page reload.
  • A second press after a visible success no longer reports "kullanıcı zaten yazar." for an entry stale state kept on screen.
  • The profile-page promote handler receives the same post-success refresh treatment.
  • Denied and errored attempts leave the current rendering untouched.
  • New coverage pins the refresh behavior for both call sites; existing suites stay green.

Acceptance criteria

  • A successful user.promote issued from ReviewerActions.onPromote refreshes both divan.roster and the open author's divan.backlog, so the promoted account and its pending items leave the screen without a full page reload.
  • With the fix, pressing promote a second time cannot report "kullanıcı zaten yazar." while a stale roster/backlog entry for that user is still rendered.
  • The profile-page promote path (apps/web/src/components/profile/PromotionActions.tsx) applies an equivalent post-success refresh mirroring the divan handler.
  • Promote attempts ending in UNAUTHORIZED, FORBIDDEN, or a transport error change neither list's rendered contents.
  • Unit tests cover the post-success refresh behavior for both call sites, and the existing divan/profile suites pass unchanged.

Original report (verbatim)

Summary

Promoting a çaylak from the divan commits correctly (tier flip + sandbox sweep in one atomic batch), but the reviewer's screen keeps showing the promoted person in divan.roster and their items in divan.backlog until the fate cache is rebuilt. The second press of "yazar yap" then reports "kullanıcı zaten yazar." against a list that should no longer contain them. Reproduced on production today.

What I was doing

Reviewing a çaylak on phoenix.kamp.us/divan as a moderator: pressed the promote button, got the success toast, then re-checked the roster after a page refresh.

What I observed

  • The mutation response is correct: user.promote returns {promoted: true} and the D1 batch (buildPromotionStatements) flips user.tier AND clears sandboxed_at on all three content tables atomically.
  • After the toast, the roster row and backlog item remain visible. Nothing in the client refetches either list: onPromote in CaylakDetail.tsx calls fate.mutations.user.promote(...) and only sets a status message.
  • These lists are not live-subscribed (plain useRequest + useListView, no /fate/live topic for divan roots), and publishPromotion publishes invalidations for pano/sozluk topics plus the user entity — nothing repairs the divan connections.
  • Verified server truth separately: a fresh logged-in session loading /divan shows the promoted account gone from the roster, so the stored state is consistent — this is purely client-side staleness.

Why it matters

A moderator who promotes one person sees a stale queue and reasonably presses promote again; the confusing "zaten yazar" message erodes trust in the review loop. Might also mask real regressions: a mod cannot tell "stale UI" from "promotion failed".

Pointers

  • apps/web/src/components/divan/CaylakDetail.tsx — ReviewerActions.onPromote (no refetch after success)
  • apps/web/src/components/divan/DivanRoster.tsx — roster read via useRequest({"divan.roster": ...})
  • apps/web/worker/features/divan/lists.ts — gated list resolvers, no live topic
  • apps/web/worker/features/pasaport/promote-live.ts — post-promote publish covers pano/sozluk/user only
  • Same pattern likely in the profile-page promote action (UserProfilePage PromotionActions, Profile 'yazarlığa yükselt' button appears on own profile, then denies #1841)

Suggested next step (non-binding)

After a successful user.promote, refetch/expire the two private roots (divan.roster, and the open divan.backlog) in the same handler; mirror it in the profile-page promote path. A guess: fate's request cache may need an explicit expire/refetch primitive since useRequest exposes no refetch handle.


Filed by an agent · session 01a02c89-7810-7c22-8280-3c8a1feb8191 · branch umut/m50-diataxis-active · 2026-08-23T06:25:17Z

Metadata

Metadata

Assignees

No one assigned

    Labels

    p0Highest priorityready-for:agentAn execution engine may pick this up.status:triagedTriage signed off; ready for write-code to picktype:bugBehavior diverges from intent

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions