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
- 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,