From ea3310ac5bda80bdb4eef250f8d4365cca76431b Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 23:34:52 +0300 Subject: [PATCH] =?UTF-8?q?fix(liste):=20=C3=BCretim=20sonras=C4=B1=20ekra?= =?UTF-8?q?n=20listeye=20kesin=20ge=C3=A7er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replaceState + router.refresh() ikilisinde Next yönlendirici kuyruğu refresh'i hiç çalıştırmıyordu (ekran 'Listen hazırlanıyor'da kalıyor, yenileyince geliyordu). Tek router.replace; 6 sn emniyet supabı (tam yükleme); 90 sn'de 'hâlâ hazırlanıyor'. Co-Authored-By: Claude Fable 5.1 --- .../rapor/components/liste-uretici.tsx | 146 ++++++++++++++---- 1 file changed, 116 insertions(+), 30 deletions(-) diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index c355e0b..6e599e7 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -10,6 +10,17 @@ // (bkz. tercih-farki.ts). Tekrar üretim riski yok: rapor varken bu bileşen // hiç render edilmez (bkz. listem-icerik.tsx), yani ?uret=1 ile dönmek ya da // sayfayı yenilemek ikinci bir harcama başlatmaz. +// +// Başarıdan sonra listeye geçiş (21 Eyl 2026 "hazırlanıyor'da kalıyor" hatası): +// eskiden `history.replaceState` + `router.refresh()` art arda çağrılıyordu. +// Server action'ın `await`i, Next'in aksiyon kuyruğu o action'ı henüz +// "bekleyen" sayarken devam ediyor; replaceState'in ürettiği RESTORE öne +// geçip action'ı "discarded" işaretliyor ama kuyruğun `last` işaretçisini +// güncellemiyor, ardından gelen REFRESH o atılmış düğümün arkasına bağlanıp +// HİÇ çalışmıyordu (bkz. next/dist/client/components/app-router-instance.js, +// dispatchAction). Sonuç: URL /listem oluyor, ekran iskelette kalıyordu. +// Kural: action cevabının hemen ardından yönlendiriciye TEK aksiyon gönder +// (router.replace); ikinci aksiyon (refresh) ancak o gezinme bitince. import { useEffect, useRef, useState, type ReactNode } from "react"; import Link from "next/link"; @@ -17,7 +28,7 @@ import { useRouter } from "next/navigation"; import { ArrowRight, Lock, RefreshCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; -import { listeOlustur } from "@/features/rapor/rapor-actions"; +import { listeOlustur, type ListeSonuc } from "@/features/rapor/rapor-actions"; import { olay, siraKovasi } from "@/lib/analitik"; import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler"; import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; @@ -25,8 +36,20 @@ import { Message } from "@/components/ui/message"; import { DanismanAvatar, DanismanBaslik } from "./sohbet-client"; import { useUretimMesaji } from "./uretim-mesaji"; +// Başarıdan sonra yönlendiricinin listeye geçmesi için tanınan süre; dolduğunda +// bileşen hâlâ ekrandaysa tam sayfa yüklemeyle geçilir (emniyet supabı). +const GECIS_SINIRI_MS = 6000; +// Tek Yapay Zeka çağrısının sunucu sınırı 60 sn (bkz. lib/ai/cagri.ts), tipik +// üretim ~20 sn. Bu eşikten sonra kullanıcıya "hâlâ hazırlanıyor" denir; istek +// kesilmez, cevap gelirse akış normal devam eder. +const UZUN_SURE_MS = 90_000; + type Durum = | { ad: "uretiliyor" } + // İstek hâlâ yolda ama beklenenden uzun sürdü: iskelet + bilgi notu + | { ad: "uzun-surdu" } + // Liste kuruldu; yönlendiricinin /listem'e geçmesi bekleniyor + | { ad: "geciliyor" } | { ad: "kredi-yetersiz" } | { ad: "hata"; mesaj: string } | { ad: "profil-yok" }; @@ -57,45 +80,85 @@ export function ListeUretici({ if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID(); setDurum({ ad: "uretiliyor" }); + // try yalnızca isteği sarar: cevap geldikten SONRAKİ bir istisna "hata" + // ekranına düşerse "Tekrar dene" taze requestId ile ikinci kez kredi + // harcatırdı. Cevap kaybolursa requestId korunur → retry idempotent. + let sonuc: ListeSonuc; try { - const sonuc = await listeOlustur({ + sonuc = await listeOlustur({ sira: profil.sira, tur: profil.tur, secimler: profil.secimler, requestId: requestIdRef.current, }); - if (sonuc.ok) { - requestIdRef.current = null; - olay("liste_olusturuldu", { - tur: profil.tur, - sira_kovasi: siraKovasi(profil.sira), - }); - // ?uret=1 URL'de kalmasın: geri dönüşte anlamsız bayrak taşınmasın. - // refresh sunucudaki raporu (ve header'daki kredi pill'ini) ekrana - // getirir. - window.history.replaceState(null, "", "/listem"); - router.refresh(); - return; - } - requestIdRef.current = null; - if (sonuc.code === "KREDI" || sonuc.code === "PAKET") { - olay("kredi_bitti"); - setDurum({ ad: "kredi-yetersiz" }); - } else if (sonuc.code === "AUTH") { - router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`); - } else { - olay("liste_uretim_hata", { kod: sonuc.code }); - setDurum({ ad: "hata", mesaj: sonuc.error }); - } } catch { olay("liste_uretim_hata", { kod: "ISTISNA" }); setDurum({ ad: "hata", mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?", }); + return; + } + + requestIdRef.current = null; + if (sonuc.ok) { + olay("liste_olusturuldu", { + tur: profil.tur, + sira_kovasi: siraKovasi(profil.sira), + }); + setDurum({ ad: "geciliyor" }); + // Next'in kendi gezinmesi: URL (?uret=1 düşer) ve sayfa ağacı birlikte + // güncellenir. Burada ikinci bir yönlendirici aksiyonu (refresh) YOK — + // neden: dosya başındaki not. Gezinme başlatılamazsa tam yükleme. + try { + router.replace("/listem"); + } catch { + window.location.replace("/listem"); + } + return; + } + if (sonuc.code === "KREDI" || sonuc.code === "PAKET") { + olay("kredi_bitti"); + setDurum({ ad: "kredi-yetersiz" }); + } else if (sonuc.code === "AUTH") { + router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`); + } else { + olay("liste_uretim_hata", { kod: sonuc.code }); + setDurum({ ad: "hata", mesaj: sonuc.error }); } } + // Emniyet supabı: liste kuruldu ama yönlendirici ekranı listeye çevirmediyse + // (bileşen hâlâ ekranda) tam sayfa yüklemeyle geç. replace: geri tuşu + // ?uret=1'e dönüp üretimi yeniden başlatmasın. Ayrı olay adı OlayAdi + // birliğini (lib/analitik.ts) genişletmeyi gerektirdiğinden supap, + // liste_uretim_hata'nın `kod: "GECIS_ZORLANDI"` değeriyle ayırt edilir. + useEffect(() => { + if (durum.ad !== "geciliyor") return; + const zamanlayici = setTimeout(() => { + olay("liste_uretim_hata", { kod: "GECIS_ZORLANDI" }); + window.location.replace("/listem"); + }, GECIS_SINIRI_MS); + return () => { + clearTimeout(zamanlayici); + // "geciliyor" son durumdur; bu temizlik yalnızca bileşen ekrandan + // kalkarken, yani gezinme işlendikten sonra koşar — kuyruk boşken + // gönderilen refresh güvenle çalışır ve layout'taki kredi pill'ini + // (gezinme layout verisini yenilemez) günceller. + router.refresh(); + }; + }, [durum.ad, router]); + + // İstek çok uzarsa kullanıcıyı bilgilendir. İstek iptal edilmez: cevap + // sonradan gelirse yukarıdaki akış durumu zaten ezer. + useEffect(() => { + if (durum.ad !== "uretiliyor") return; + const zamanlayici = setTimeout(() => { + setDurum((d) => (d.ad === "uretiliyor" ? { ad: "uzun-surdu" } : d)); + }, UZUN_SURE_MS); + return () => clearTimeout(zamanlayici); + }, [durum.ad]); + useEffect(() => { if (basladi.current) return; basladi.current = true; @@ -170,10 +233,10 @@ export function ListeUretici({ ); } - return ; + return ; } -function UretimGovdesi() { +function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { const mesaj = useUretimMesaji(); return ( @@ -182,9 +245,32 @@ function UretimGovdesi() {

Listen hazırlanıyor

-

- Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. -

+ {uzunSurdu ? ( + <> +

+ Bu sefer beklenenden uzun sürdü, listen hâlâ hazırlanıyor. Bu + sayfada bekleyebilir ya da hazır olup olmadığına bakabilirsin; + bakmak kredi harcamaz. +

+ {/* ?uret=1'siz /listem üretimi asla başlatmaz: rapor hazırsa + liste, değilse boş durum gelir — kredi ikinci kez yanmaz. + Tam yükleme: yönlendirici kuyruğu bekleyen action'ın arkasında + tıkalı olduğundan router.refresh burada işe yaramaz. replace: + geri tuşu ?uret=1'e dönüp üretimi yeniden başlatmasın. */} + + + ) : ( +

+ Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. +

+ )} {/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,