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
|
// (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
|
// hiç render edilmez (bkz. listem-icerik.tsx), yani ?uret=1 ile dönmek ya da
|
||||||
// sayfayı yenilemek ikinci bir harcama başlatmaz.
|
// 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 { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -17,7 +28,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { ArrowRight, Lock, RefreshCcw } from "lucide-react";
|
import { ArrowRight, Lock, RefreshCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
@@ -25,8 +36,20 @@ import { Message } from "@/components/ui/message";
|
|||||||
import { DanismanAvatar, DanismanBaslik } from "./sohbet-client";
|
import { DanismanAvatar, DanismanBaslik } from "./sohbet-client";
|
||||||
import { useUretimMesaji } from "./uretim-mesaji";
|
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 =
|
type Durum =
|
||||||
| { ad: "uretiliyor" }
|
| { 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: "kredi-yetersiz" }
|
||||||
| { ad: "hata"; mesaj: string }
|
| { ad: "hata"; mesaj: string }
|
||||||
| { ad: "profil-yok" };
|
| { ad: "profil-yok" };
|
||||||
@@ -57,45 +80,85 @@ export function ListeUretici({
|
|||||||
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||||
setDurum({ ad: "uretiliyor" });
|
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 {
|
try {
|
||||||
const sonuc = await listeOlustur({
|
sonuc = await listeOlustur({
|
||||||
sira: profil.sira,
|
sira: profil.sira,
|
||||||
tur: profil.tur,
|
tur: profil.tur,
|
||||||
secimler: profil.secimler,
|
secimler: profil.secimler,
|
||||||
requestId: requestIdRef.current,
|
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 {
|
} catch {
|
||||||
olay("liste_uretim_hata", { kod: "ISTISNA" });
|
olay("liste_uretim_hata", { kod: "ISTISNA" });
|
||||||
setDurum({
|
setDurum({
|
||||||
ad: "hata",
|
ad: "hata",
|
||||||
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
|
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(() => {
|
useEffect(() => {
|
||||||
if (basladi.current) return;
|
if (basladi.current) return;
|
||||||
basladi.current = true;
|
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();
|
const mesaj = useUretimMesaji();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -182,9 +245,32 @@ function UretimGovdesi() {
|
|||||||
<h1 className="font-heading text-xl font-bold sm:text-2xl">
|
<h1 className="font-heading text-xl font-bold sm:text-2xl">
|
||||||
Listen hazırlanıyor
|
Listen hazırlanıyor
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-slate-400">
|
{uzunSurdu ? (
|
||||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
<>
|
||||||
</p>
|
<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>
|
</header>
|
||||||
|
|
||||||
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
||||||
|
|||||||
Reference in New Issue
Block a user