fix(liste): üretim sonrası ekran listeye kesin geçer
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <UretimGovdesi />;
|
||||
return <UretimGovdesi uzunSurdu={durum.ad === "uzun-surdu"} />;
|
||||
}
|
||||
|
||||
function UretimGovdesi() {
|
||||
function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) {
|
||||
const mesaj = useUretimMesaji();
|
||||
|
||||
return (
|
||||
@@ -182,9 +245,32 @@ function UretimGovdesi() {
|
||||
<h1 className="font-heading text-xl font-bold sm:text-2xl">
|
||||
Listen hazırlanıyor
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400">
|
||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||
</p>
|
||||
{uzunSurdu ? (
|
||||
<>
|
||||
<p className="max-w-md text-sm text-slate-600">
|
||||
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.
|
||||
</p>
|
||||
{/* ?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. */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.replace("/listem")}
|
||||
className="mt-2 cursor-pointer"
|
||||
>
|
||||
Listemi kontrol et
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400">
|
||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
||||
|
||||
Reference in New Issue
Block a user