Files
kolaytercih/docs/gece-vardiyasi/2026-09-21/06-cto-plani.md
bilalgursen 0ab4dbe88c
All checks were successful
Deploy / deploy (push) Successful in 7m18s
Enhance Traefik configuration for www.kolaytercih.com
Added a new router and middleware for handling www to apex domain redirection, ensuring that Let's Encrypt certificates are obtained for both domains. Updated the docker-compose.yml file to include necessary rules and middleware for proper redirection and TLS handling.

Also, updated the CEO agent description to remove redundant details and clarified the QA engineer's responsibilities regarding testing critical flows and error reporting. Adjusted the CTO agent's section for consistency in expert guidance and clarified the designer's tools to include 'Skill' for better design resource management.
2026-09-21 20:37:12 +03:00

181 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 — CTO planı — 2026-09-21 gece vardiyası (Tur 3)
Hazırlayan: CTO ajanı · Girdi: `05-ceo-kararlari.md` §3 + `01`/`02`/`03`/`04` · Kod **değiştirilmedi**; yalnızca bu dosya yazıldı. Tüm dosya:satır atıfları main HEAD `56925e1` üzerinde okunarak doğrulandı. (Tek yan etki: `p4-eposta` worktree'sinde doğrulama için `next typegen` + `tsc` + `lint` koşuldu; yalnızca gitignore'lu `.next/` ve `next-env.d.ts` oluştu, `git status` temiz.)
## 1. Özet (5 madde)
1. **Dört paket de uygulanabilir; dosya kümeleri ayrık.** CEO'nun listesindeki üç gizli çakışma çözüldü: (a) P3'ün "tek sabit"i `seo.tsx`'e (P1'in) giremez → yeni dosya; (b) P2'nin `paket_goruntulendi` olayı `/paket` sayfasına (P3'ün) dokunmadan yeni `src/app/paket/layout.tsx` ile bağlanır; (c) P2 `src/app/layout.tsx`'e dokunmaz, erken olay kaybı `analitik.ts` içinde kuyrukla çözülür.
2. **P2 kök nedeni doğrulandı ve bir tane daha bulundu.** Hook `useRef` + `[ad, anahtarEki]` deps'li effect kullanıyor; eleman ilk commit'te yoksa (banner) ya da sökülüp yeniden takılıyorsa (tadımlık) gözlem hiç kurulmuyor → callback ref. Ek bulgu: Rybbit script'i `afterInteractive`; `olay()` script yokken sessizce düşürüyor → mount anında atılacak yeni olaylar (`sonuc_/paket_goruntulendi`) sert yüklemede kaybolur → küçük kuyruk şart.
3. **P3'te CEO'nun kapsamı eksik (CTO itirazı #1).** "İhtimal dili" yalnız `riskNotuUret`'te değil: tadımlık havuzunun `riskNotu`'su **LLM üretimi ve DB'de kayıtlı**, prompt'u açıkça "yüksek/orta/düşük ihtimal dili kullan" diyor (`scripts/tadimlik-uret.ts:70-71`); danışman sohbetinin sistem prompt'u da aynı (`src/app/api/soru/route.ts:48`). Girişsiz ziyaretçinin /sonuc'ta gördüğü tek yapay zekâ satırı eski dille kalırdı. Çözüm: risk cümlesi tek saf fonksiyona (`riskNotuMetni`) alınır, tadımlıkta **okuma anında** deterministik üretilir — prod'da LLM yeniden koşusu gerekmez.
4. **P4'te soğuma kaynağı değiştirildi (CTO itirazı #2).** better-auth 1.6.23'te `verification.identifier` = **token** (e-posta değil); e-posta yalnız `value` JSON'unda ve satır `sendMagicLink` çağrılmadan **önce** yazılıyor → tablo tabanlı soğuma kendi satırına takılır ve kayan pencere üretir. Yerine süreç içi `Map` (tek konteyner; şema/DB yazımı yok). Ayrıca prod guard **modül yüklenirken değil gönderim anında** atmalı; aksi hâlde `next build` (NODE_ENV=production, env'siz) kırılır.
5. **Doğrulama tuzağı:** taze worktree'de `pnpm exec tsc --noEmit` **baseline'da kırık** (`PageProps` global tipi `.next/types`'tan geliyor). Önce `pnpm exec next typegen` koşulmalı — denendi: typegen → tsc 0 hata, `pnpm lint` temiz. Tam `pnpm build`'i yalnız P1 koşar (HTML kabul kriteri); dört paralel build ~8 GB RAM ister, diğerleri tsc+lint ile kapanır, build'i Tur 4'te orkestratör sırayla alır.
### Dosya sahipliği (ayrık; listede olmayan dosyaya dokunulmaz)
| Paket | Dosyalar (**Y** = yeni) |
|---|---|
| **P1** `gece/p1-rehber-altyapi` | `src/features/rehber/components/el-cizimi-sema.tsx` · `src/features/rehber/components/rehber-makale.tsx` · `src/lib/rehber.ts` · `src/lib/tarih.ts` **Y** · `src/app/sitemap.ts` · `src/lib/seo.tsx` (yalnız `articleJsonLd`) |
| **P2** `gece/p2-olcum` | `src/components/use-gorunum-olayi.ts` · `src/lib/analitik.ts` · `src/features/sihirbaz/components/{hero-form,cta-sira-form,sihirbaz-cagri-karti}.tsx` · `src/features/liste/components/tercih-profili-kapisi.tsx` · `src/features/kullanici/components/giris-form.tsx` · `src/features/rapor/components/{liste-uretici,listem-govde}.tsx` · `src/app/paket/layout.tsx` **Y** · `src/features/odeme/components/paket-gorunum-olayi.tsx` **Y** · `src/components/rybbit-identify.tsx` · `src/features/kullanici/components/user-nav.tsx` · (salt-okunur doğrulama, değişiklik beklenmiyor: `kaydet-banner.tsx`, `tadimlik-satiri.tsx`) |
| **P3** `gece/p3-vaat-dili` | `src/app/page.tsx` · `src/app/paket/page.tsx` · `src/features/odeme/components/paket-satinal.tsx` · `src/lib/ai/rapor.ts` (yalnız `riskNotuUret`) · `src/app/layout.tsx` (yalnız `description`) · `src/lib/veri-kapsami.ts` **Y** · **CTO eki:** `src/lib/risk.ts` · `src/lib/tadimlik-havuzu.ts` · `scripts/tadimlik-uret.ts` · `src/app/api/soru/route.ts` (yalnız 48. satır) · `src/components/arama-funnel-karti.tsx` (yalnız 38. satır) · `src/features/katalog/components/veri-kaynagi.tsx` (yalnız `export`) |
| **P4** `gece/p4-eposta` | `src/lib/eposta-gonder.ts` · `src/lib/eposta.ts` · `src/lib/auth.ts` · `src/lib/analitik-sunucu.ts` · isteğe bağlı ayrı commit: `.gitea/workflows/ci.yaml` |
Çakışma kararları: `seo.tsx` → P1 (P3 oraya sabit eklemez). `src/app/paket/page.tsx` + `paket-satinal.tsx` → P3 (P2 yeni layout dosyası kullanır). `src/app/layout.tsx` → P3 (P2 script'e dokunmaz). `tadimlik-satiri.tsx` → P2, `tadimlik-havuzu.ts` → P3. `analitik.ts` → P2, `analitik-sunucu.ts` → P4. `giris-form.tsx` → P2, `auth.ts` → P4.
### Tüm yazılımcılar için ortak kurallar
- Yalnız kendi worktree + dalında lokal commit; **push/merge yok**. `git add -A` / `git add .` **yasak** — yalnız sahip olunan dosyalar yol vererek eklenir (build `data/*.db-shm` gibi izlenen dosyaları kirletebilir; `data/app.db`, `docker-compose.yml`, `docs/geo/geo-raporu.md` asla).
- Doğrulama sırası (worktree kökünde): `pnpm exec next typegen && pnpm exec tsc --noEmit && pnpm lint`. Dev sunucusu/tarayıcı yok (AGENTS.md).
- Next'e özgü API'den önce `node_modules/next/dist/docs/` (P1: `01-app/03-api-reference/03-file-conventions/01-metadata/sitemap.md`, `04-functions/generate-metadata.md`; P2: `03-file-conventions/layout.md`). Doğrulananlar: `openGraph.modifiedTime` tipte var (`opengraph-types.d.ts:28`); sitemap `lastModified: string | Date`; `next typegen` (`06-cli/next.md:175`).
- Commit mesajı sonu: `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`. Rapor: `07-yazilimci-raporlari.md`'ye kendi başlığı.
---
## 2. P1 · Rehber altyapısı
**Amaç:** Şema metni JS'siz botlara görünsün; mermaid hatasında sonsuz iskelet bitsin; `guncelleme` tarihi uçtan uca aksın; frontmatter tırnağı ve tablo taşması düzelsin.
**Kök neden (kanıt):**
- `el-cizimi-sema.tsx:85-97`: `aria-label` yalnız `svg` state'i dolunca render ediliyor; SSR çıktısı = `aria-hidden` iskelet + figcaption. Bileşen client ama SSR edilir → içine konan `<details>` sunucu HTML'ine girer.
- `el-cizimi-sema.tsx:55-74`: `import("mermaid").then(async …)` zincirinde `catch` yok; `mermaid.render` atarsa (`getBBox is not a function`, 2 oturum — tarayıcıya özgü, kaynağı saptanamadı) `svg` hiç dolmaz, iskelet kalır, promise yakalanmaz. Mermaid ayrıca `#d<id>` geçici div'ini `body`'de bırakabilir (`mermaid.core.mjs:1248`).
- `rehber.ts:71-74`: `.replace(/^["']|["']$/g, "")` iki ucu bağımsız siliyor. Etkilenen yalnız 2 dosya (`merkezi-yerlestirme…md:3`, `taban-siralamalar…md:3`); 24 dosyanın hiçbirinde tam tırnaklı değer yok.
- `rehber.ts:20-26` `RehberOzet`'te `guncelleme` yok; `sitemap.ts:22` `/rehber` girdisinde `lastModified` yok, `:47` `y.tarih`; `seo.tsx:149-169` `dateModified` yok; `rehber-makale.tsx:32` yalnız `publishedTime`, gövdede tarih yok.
- `rehber-makale.tsx:79`: `[&_table]:w-full`, sarmalayıcı yok.
**Değişiklikler:**
1. `src/lib/tarih.ts` **(yeni, saf, `server-only` yok):** `export function trTarih(iso: string): string` — `/^(\d{4})-(\d{2})-(\d{2})$/` ile ayrıştır, sabit `AYLAR = ["Ocak",…,"Aralık"]` dizisinden `"21 Eylül 2026"` üret; biçim uymazsa `throw`. `Date`/`toLocale*`/saat dilimi **kullanma**.
2. `src/lib/rehber.ts`:
- Tırnak: değer uzunluğu ≥ 2, ilk ve son karakter **aynı** tırnak (`"` ya da `'`) **ve** aradaki metin o tırnağı içermiyorsa soy; aksi hâlde olduğu gibi bırak.
- `RehberOzet`'e `guncelleme?: string`. `metaDogrula`: `tarih` ve (varsa) `guncelleme` `YYYY-MM-DD` değilse ya da `guncelleme < tarih` ise build'i kıran `Error` (mevcut hata mesajı kalıbıyla). `oku()` ve `ozetOku()` (cache'ten kırpma dalı dahil, `:169-170`) alanı taşır. Sıralama `tarih`'e göre **kalır**. `RehberAramaKaydi` değişmez.
- Tablolar: `parcala()` içindeki iki `marked.parse` çıktısını tek yardımcıdan geçir: `<table>` → `<div class="rehber-tablo" role="region" aria-label="Tablo" tabindex="0"><table>`, `</table>` → `</table></div>` (marked 18 `<table>`'ı özniteliksiz basar; global `marked.use` **kullanma**). `:12` yorumundaki "10 makale"yi düzelt.
3. `src/lib/seo.tsx` — yalnız `articleJsonLd`: `dateModified?: string` parametresi; çıktıda `dateModified: opts.dateModified ?? opts.datePublished`.
4. `src/features/rehber/components/rehber-makale.tsx`: metadata'da `openGraph: { type: "article", publishedTime: yazi.tarih, modifiedTime: yazi.guncelleme ?? yazi.tarih }`; `articleJsonLd`'ye `dateModified`; `aciklama` paragrafının altına `<p className="mt-3 text-xs text-slate-500">Yayın: <time dateTime={yazi.tarih}>{trTarih(yazi.tarih)}</time>` + (yalnız `guncelleme` varsa) ` · Güncelleme: <time …>` — düz metin satırı, **badge değil** (SectionEyebrow kuralı tetiklenmez). Sınıf listesinde `[&_table]:mt-4` → `[&_.rehber-tablo]:mt-4 [&_.rehber-tablo]:overflow-x-auto`.
5. `src/app/sitemap.ts`: rehber girdilerinde `lastModified: y.guncelleme ?? y.tarih`; `/rehber` girdisini `staticEntries`'ten fonksiyon içine al, `lastModified` = tüm yazıların `guncelleme ?? tarih` değerlerinin en büyüğü (ISO string karşılaştırması yeter).
6. `src/features/rehber/components/el-cizimi-sema.tsx`:
- `const [hata, setHata] = useState(false)`. `cizdir()` → `import("mermaid").then(async …).catch(() => { document.getElementById("d" + semaId)?.remove(); if (!iptal) setHata(true); })`; `mermaid.initialize`'a `suppressErrorRendering: true` (11.16'da var, `config.type.d.ts:237`).
- Render: `svg` varsa mevcut `role="img" aria-label` div'i (korunur); yoksa ve `hata` değilse iskelet; `hata` ise iskelet **çizilmez**. figcaption'dan sonra, `<figure>` içinde: `<details open={hata || undefined} className="mt-2 text-xs text-slate-500"><summary className="cursor-pointer font-medium text-slate-600">Şemayı metin olarak oku</summary><p className="mt-2 text-sm leading-6 text-slate-600">{ariaLabel}</p></details>`. Gizli metin (`sr-only`, `display:none`) **yok**.
**Commit bölünmesi:** (1) `fix(rehber): frontmatter tırnak + tablo sarmalayıcı` · (2) `feat(rehber): guncelleme alanı → sitemap/JSON-LD/OG/görünür tarih` · (3) `fix(rehber): şema metni SSR'da + mermaid hata toparlama`. Hepsi [KARAR].
**Kabul kriterleri / doğrulama:** typegen+tsc+lint; ardından **yalnız bu pakette** `pnpm build`, sonra (ana repodaki mevcut build'de baseline ölçüldü: ilk iki grep **0**, description `Üst sıraya…` diye tırnaksız başlıyor — yani kriterler ayırt edici):
- `grep -c "Şemayı metin olarak oku" .next/server/app/rehber/tercih-listesi-nasil-yapilir.html` ≥ 1 ve aynı dosyada `grep -c "Tercih listesi dilim şeması"` ≥ 1; `.next/server/app/meraklisina.html`'de `grep -c "Sihirbaz akış şeması"` ≥ 1.
- `grep -o 'name="description" content="[^"]\{0,30\}' .next/server/app/rehber/merkezi-yerlestirme-nasil-calisir.html` → `&quot;Üst sıraya` ile başlar (aynısı `taban-siralamalar-nasil-degisir`).
- Aynı HTML'de `rehber-tablo` sınıfı ve `Yayın:` + `<time dateTime="2026-` bulunur; `.next/server/app/sitemap.xml.body`'de `/rehber</loc>` girdisinin `<lastmod>`'u var. Geçici olarak bir yazıya `guncelleme: 2026-09-22` ekleyip `dateModified`/`modifiedTime`/`Güncelleme:` çıktısını gör, **commit'lemeden geri al** (`content/` yazar dalının).
- Kod incelemesi: `.catch` var, hata dalında iskelet yok, `details` açık.
**Kapsam dışı:** `kume`/`ilgili` (T4), FAQPage, llms.txt, IndexNow, flowchart dışı mermaid, font, yazar alanı, `content/rehber/*` (yazar dalı), `rehber-listesi.tsx`.
**Riskler / AGENTS.md:** `ElCizimiSema` `/meraklisina` hunisiyle ortak → `<details>` orada da çıkar; metin kaynağı yine tek `ariaLabel` prop'u olduğundan **şema senkron kuralına yeni yük binmez**, `huni-semasi.tsx`'e dokunulmaz; Bilal görsel bakmalı. Ekran okuyucuda metin iki kez okunabilir (CEO kabul etti). `metaDogrula` sıkılaşması: 24 dosyanın hepsi `YYYY-MM-DD` (doğrulandı), yazar dalındaki yeni dosyalar bozuk tarihle gelirse build kırılır — istenen davranış.
---
## 3. P2 · Ölçüm onarımı
**Amaç:** Görünüm hook'u sonradan mount olan elemanda da çalışsın; 6 yeni olay; 429 mesajı; PII ayrı commit.
**Kök neden (kanıt):**
- `use-gorunum-olayi.ts:20,28-30,51`: `useRef` + deps `[ad, anahtarEki]`. `ref.current` effect anında `null` ise erken dönüyor ve deps değişmediği için bir daha çalışmıyor.
- `kaydet-banner.tsx:33,47,57`: `kapali` başlangıçta `true` → ilk render `null`; üstelik içerik `ViewportPortal` içinde (`viewport-portal.tsx` — ilk client render'da da `null`). Eleman hiçbir zaman ilk commit'te yok → olay **0**.
- `tadimlik-satiri.tsx:75-77,87,104`: tip uymuyorsa `setSatir(null)` → `<section>` sökülür, fetch sonrası **yeni** DOM düğümü takılır; gözlemci eski (kopuk) düğümde kalır.
- Çalışanlar (`listem-govde.tsx:139-148`, `sihirbaz-cagri-karti.tsx:232`) eleman ilk commit'te var olduğu için etkilenmiyor.
- **İkinci neden (raporlarda yok):** `layout.tsx:129-136` script'i `afterInteractive`; `analitik.ts:52-58` `window.rybbit` yoksa sessiz no-op. Sert sayfa yüklemesinde mount effect'i script'ten önce koşar → olay kaybolur. Mevcut olaylar kullanıcı etkileşimiyle atıldığı için görünmedi; yeni "sayfa görüntülendi" olaylarını doğrudan vurur.
- **Uyarı:** `tadimlik_goruntulendi` = 0'ın tek nedeni hook olmayabilir — prod `tadimlik_havuzu` boşsa bileşen hiç render edilmez (`tadimlik-bolumu.tsx:20`, bellek notu "deploy sonrası prod'da `pnpm tadimlik`") ve bölüm uzun tablonun altında. Düzeltmeden sonra hâlâ 0 ise önce havuz kontrol edilir (Bilal).
**Değişiklikler:**
1. `src/lib/analitik.ts`: `OlayAdi`'na **tam bu adlarla** ekle: `cta_form_goruntulendi`, `sira_form_hata`, `sonuc_goruntulendi`, `paket_goruntulendi`, `liste_uretim_hata`, `giris_hata`. `olay()`: `window.rybbit` yoksa olayı modül içi kuyruğa (en çok 20) koy; 500 ms aralıkla en çok 20 deneme (10 sn) — script gelince sırayla boşalt, gelmezse kuyruğu at (reklam engelleyici). Yeni `export function olayBirKez(ad, ozellikler?, anahtarEki?)`: hook'taki `sessionStorage` anahtar mantığının (`kolaytercih.olay.<ad>[.<ek>]`) aynısı — anahtar varsa çık, yoksa yaz + `olay()`; storage hatasında yine gönder.
2. `src/components/use-gorunum-olayi.ts`: dönüş tipi `RefCallback<T>`; `useCallback((el: T | null) => { … }, [ad, anahtarEki])`. `el` yoksa / `IntersectionObserver` yoksa / anahtar zaten yazılıysa çık; yoksa gözlemci kur, kesişimde `io.disconnect()` + `olayBirKez`. React 19 ref temizleme fonksiyonu döndür (`return () => io.disconnect()`). `ozelliklerRef` deseni korunur. Çağıranların hepsi yalnız `ref={…}` veriyor (`.current` okuyan yok — doğrulandı) → 5 kullanım yerinde değişiklik gerekmez.
3. `cta-sira-form.tsx`: **yalnız profilsiz daldaki** `<section>`'a (`:162`) `ref={useGorunumOlayi<HTMLElement>("cta_form_goruntulendi")}` (hook çağrısı koşulsuz, üstte). Özellik yok — Rybbit pathname'i zaten yazar. `catch` dalına (`:110`) `olay("sira_form_hata", { kaynak: "cta" })`.
4. `hero-form.tsx:87` catch → `{ kaynak: "hero" }`; `tercih-profili-kapisi.tsx` `facetYukle` catch → `{ kaynak: duzenle ? "duzenle" : "kapi" }`. Doğrulama hatası (geçersiz sayı) **sayılmaz**, yalnız istek hatası.
5. `sihirbaz-cagri-karti.tsx` `SihirbazBolumu`: mount effect'inde `olayBirKez("sonuc_goruntulendi", { girisli: girisliMi ? "evet" : "hayir", sira_kovasi: siraKovasi(sira) }, girisliMi ? "girisli" : "anonim")`.
6. `/paket`: `src/features/odeme/components/paket-gorunum-olayi.tsx` **(yeni, client, `null` döner)** — effect'te `new URLSearchParams(window.location.search).get("kaynak")`, allowlist `kilit | kredi_bitti | nav`, değilse `"diger"`; `olayBirKez("paket_goruntulendi", { kaynak }, kaynak)`. `useSearchParams` **kullanma** (`cacheComponents: true` → Suspense/CSR bailout derdi). `src/app/paket/layout.tsx` **(yeni)**: `{children}` + `<PaketGorunumOlayi />`. Linkler: `listem-govde.tsx:288,369` → `/paket?kaynak=kilit`; `liste-uretici.tsx:134` → `?kaynak=kredi_bitti`; `user-nav.tsx:51` → `?kaynak=nav`.
7. `liste-uretici.tsx`: `else` dalı (`:87`) `olay("liste_uretim_hata", { kod: sonuc.code })`; `catch` (`:89`) `{ kod: "ISTISNA" }`. `AUTH`/`KREDI`/`PAKET` dalları bu olayı **atmaz**.
8. `giris-form.tsx`: `linkGonder` hata dalı → `olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 })`; `status === 429` ise toast "Çok sık denedin, bir dakika sonra tekrar dene", değilse mevcut mesaj. Google hata dalı → `{ yontem: "google", status }`. `geriSayimBaslat` 30 → **60** sn (P4'ün sunucu soğumasıyla hizalı; P4 merge edilmese de zararsız).
9. **Ayrı, son commit [BİLAL]:** `rybbit-identify.tsx` `eposta`/`isim` prop'ları ve trait'ler kalkar, `kullaniciTanit(kullaniciId)`; `user-nav.tsx:47` çağrısı sadeleşir. `analitik.ts`'e dokunmaz.
**Commit bölünmesi:** (1) `fix(analitik): görünüm hook'u callback ref + erken olay kuyruğu` · (2) `feat(analitik): huni olayları` (3–7 + 8'in olay kısmı) · (3) `fix(giris): 429 mesajı + 60 sn geri sayım` · (4) **[BİLAL]** `chore(gizlilik): rybbit identify yalnız kullanıcı ID`. (4) en sonda durmalı: `user-nav.tsx`'te 47. ve 51. satırlar yakın; Bilal reddederse `git merge gece/p2-olcum~1`.
**Kabul kriterleri:** typegen+tsc+lint temiz. `grep -rn "useGorunumOlayi" src` → mevcut 5 + `cta-sira-form`. `grep -rnE "olay(BirKez)?\(" src | grep -iE "email|eposta:|sira:"` → boş (ham sıra/e-posta yok; sıra yalnız `siraKovasi()`). Olay adları yukarıdaki listeyle birebir (kalıcı sözleşme). Kod incelemesi: hook eleman `null→düğüm` geçişinde gözlem kuruyor, düğüm sökülünce `disconnect` ediyor.
**Kapsam dışı:** `giris_basarili` (P4), `odeme_hata`, `sihirbaz_kapatildi`, `rapor_yazdirildi`, /sonuc pageview şişmesi, iç trafik filtresi, `src/app/layout.tsx`, `page.tsx`/`sohbet-client.tsx`/`tercih-degisti-modali.tsx` içindeki `/paket` linkleri (kaynak = `diger` düşer), `rapor-actions.ts` (hata alt nedeni).
**Riskler / AGENTS.md:** Yeni CTA butonu eklenmiyor; var olan `ArrowRight` desenine dokunma. `?kaynak=` `/paket` canonical'ını etkilemez (`sayfaMetadata path: "/paket"`). `/giris?callback=/paket` dönüşünde kaynak kaybolur (kabul). Kuyruk sayesinde dev'de (script yok) 10 sn sonra sessizce boşalır — hata yok.
---
## 4. P3 · Vaat dili ve güven temizliği
**Amaç:** Olmayan özelliği ("yerleşme olasılığı") satan dili kaldırmak; 4↔5 yıl tutarsızlığını bitirmek; iki [BİLAL] değişikliğini bağımsız commit olarak hazırlamak.
**Kök neden (kanıt):** `page.tsx:180`, `paket-satinal.tsx:31` "yerleşme olasılığı"; `rapor.ts:166,168,170,172` dört cümlenin dördü de "ihtimal". "4 yıl": `page.tsx:107,122,148,180,365` + `paket-satinal.tsx:31`; DB `sira2021…sira2025` = 5 (`rapor.ts` `programBilgisi`, tablolar "Son 5 yıl"). **CEO listesinde olmayanlar:** `scripts/tadimlik-uret.ts:70-71` (prompt "ihtimal dili kullan"), `:72` ("son 4 yılın"), `:84-87` (mock); `src/app/api/soru/route.ts:48`; `src/components/arama-funnel-karti.tsx:38` "Yerleşme ihtimalini görerek seç." Tadımlık `riskNotu` DB'de (`schema.ts:187`) ve `tadimlik-havuzu.ts:83`'ten olduğu gibi servis ediliyor; oysa rozet aynı satırda canlı hesaplanıyor (`tadimlik-satiri.tsx:107`) → metinle rozet çelişebiliyor. CEO'nun grep'i (`olasılı|ihtimalle yerleş`) "ihtimalini/ihtimal dili/ihtimali düşük" kalıplarını yakalamıyor.
**Commit 1 — [KARAR] `fix(metin): olasılık dili + 5 yıl`:**
- `src/lib/veri-kapsami.ts` **(yeni):** `export const VERI_YIL_SAYISI = 5;` (+ yorum: DB `sira2021–sira2025`; veri yılı eklenince güncelle). **`seo.tsx`'e koyma (P1'in).**
- `src/lib/risk.ts`: `export function riskNotuMetni(risk: RiskSeviyesi | null): string` — güvenli: "Son taban sıralaması senin sıranın belirgin altında; geçmiş veriye göre güvenli aralıkta." · az-riskli: "Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir." · riskli: "Son taban sıralaması senin sıranın üstünde kapandı; hayal tercihi olarak düşün." · `null`: "Güncel taban verisi yok; bu satırı temkinli değerlendir." ("Geçen yıl" **deme**: kod `sira2025 ?? sira2024`'e düşüyor.)
- `src/lib/ai/rapor.ts` `riskNotuUret`: gövde → `return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira));`. Başka hiçbir şeye dokunma (5/13/6 şeması, risk eşiği, prompt aynı).
- `page.tsx:180` ve `paket-satinal.tsx:31` → `` `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)` ``; `page.tsx:107,122,148,365` sabitten. `page.tsx`'te import'u **32. satırın (`@/lib/credits`) hemen altına** koy (commit 3'ün dokunacağı 15. ve 55-59. satırlardan uzak).
- `arama-funnel-karti.tsx:38` → "Yerleşme riskini görerek seç."
**Commit 1b — [KARAR · CTO eki] `fix(ai): tadımlık risk notu deterministik + prompt dili`:**
- `tadimlik-havuzu.ts:83`: `riskNotu: riskNotuMetni(riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim))` (`program` = satırın snapshot'ı; üç fonksiyon `@/lib/risk`). Böylece `/api/tadimlik` de kapsanır; DB satırları ve şema değişmez.
- `scripts/tadimlik-uret.ts:70-72` prompt: "riskNotu: geçmiş taban verisine dayalı risk değerlendirmesi (TEK kısa cümle). ASLA garanti/kesin/yüzde/olasılık/ihtimal deme; 'güvenli / az riskli / riskli' etiketleriyle konuş." + "son 5 yılın"; mock metinleri aynı dile. Script **koşulmaz**.
- `api/soru/route.ts:48` → "ASLA yerleşme garantisi verme; olasılık, yüzde ya da 'ihtimal' dili kullanma — güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş."
**Commit 2 — [BİLAL] `chore(metin): danışman fiyat kıyası`:** "2.000–10.000 TL" → "1.000–5.000 TL" (`page.tsx:90,168,484`, `paket/page.tsx:52`); "onda bir fiyatına" → "çok daha düşük fiyata" (`paket/page.tsx:49`, `layout.tsx:55`; `page.tsx:228` yorum satırı içinde ölü kod — yorum bloğunu sil). Fiyat bölümü paragrafının (`page.tsx:484-487`) altına `text-xs text-slate-500` kaynak satırı: "Danışman fiyat aralığı: Armut, 2026." Sabit **kullanma**, düz metin (commit 1'den bağımsız kalsın). Rakamı ajanlar doğrulamadı → Bilal Armut sayfasını açmadan almasın.
**Commit 3 — [BİLAL] `chore(guven): hero'da YÖK logosu yerine tarihli veri satırı`:** `veri-kaynagi.tsx:4` `const TARIH_METNI` → `export const KATALOG_TARIH_METNI` (dosya içi kullanım güncellenir; tek biçim kaynağı). `page.tsx:236-251`: `<Image>` + dikey ayraç kalkar; aynı kutu (`inline-flex … rounded-lg border …` sınıfları **aynen**) içinde `<strong>Veri: YÖK Atlas</strong> · son güncelleme <time dateTime={KATALOG_VERI_TARIHI}>{KATALOG_TARIH_METNI}</time>`. `import Image` (15) silinir; `KATALOG_VERI_TARIHI` 55-59'daki seo import'una eklenir. `public/yok-logo.svg` **silinmez**.
**Kabul kriterleri:** typegen+tsc+lint. Commit 1+1b sonrası: `grep -rniE "olasılı|ihtimal" src scripts/tadimlik-uret.ts` → yalnız `page.tsx:76` ("binlerce ihtimal" — kombinasyon anlamı, vaat değil), `page.tsx:132` ("boşta kalma ihtimalini" — SSS, kapsam dışı), `program-tablosu.tsx:176` (kod yorumu) ve prompt'lardaki **yasaklayıcı** cümleler. `grep -rn "4 yıl" src/app/page.tsx src/features/odeme` → boş. Commit 2 sonrası `grep -rn "onda bir\|2\.000–10\.000" src` → boş. Bağımsızlık testi: `main`'den geçici dal açıp commit 2'yi ve 3'ü **tek başlarına** `git cherry-pick` et → çakışmasız + tsc geçer; geçici dalı sil.
**Kapsam dışı:** Garanti→Güvenli (H2) — `cta-sira-form.tsx`'teki "garanti dilimleri / nereye yerleşirsin" dahil (dosya P2'nin); `/hakkinda`, "Sınırlarımız", hero/kapanış tonu, SSS metinleri, `vizyon.md`.
**Riskler / AGENTS.md:** Davranış değişmiyor (eşikler, 5/13/6, havuz kuralları aynı) → **`huni-semasi.tsx` `SEMA` güncellenmez**; şemadaki "risk notu gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir" iddiası 1b ile tadımlık için de doğru hâle gelir. Kayıtlı raporlardaki eski cümleler kalır (göç yok). Commit 3 yeni badge tasarlamaz (SectionEyebrow kuralı: mevcut bilgi kutusu, pill değil). CTA butonlarına dokunulmuyor.
---
## 5. P4 · Giriş e-postası sağlamlaştırma
**Amaç:** Reply-To, prod'da sessiz fallback'in bitmesi, adres başına soğuma, şablon temizliği, `giris_basarili`.
**Kök neden (kanıt):** `eposta-gonder.ts:13-18` ve `auth.ts:33-37` `NODE_ENV`'e bakmadan konsola yazıp başarı dönüyor (token'lı link loga düşer); `eposta-gonder.ts:22` sessiz `onboarding@resend.dev`; `:21-29` `replyTo` yok; `eposta.ts:79-80` Google Fonts `<link>`, `:96-101` footer'da iletişim yok; `auth.ts`'te adres bazlı sınır yok (yalnız eklentinin IP limiti 5/60 sn, `magic-link/index.mjs:157-163`); `analitik-sunucu.ts:17` tek olay.
**Değişiklikler:**
1. `eposta-gonder.ts`: `export function epostaKonsolModu(): boolean` = `!process.env.RESEND_API_KEY && (process.env.NODE_ENV !== "production" || process.env.EPOSTA_KONSOL_FALLBACK === "1")`. `epostaGonder` başında, **çağrı anında** (modül seviyesinde **asla** — `next build` NODE_ENV=production + env'siz koşar, modül yüklenirken atmak build'i kırar): konsol modu → mevcut `console.log`; anahtar yok ve konsol modu değil → `throw new Error("[eposta] RESEND_API_KEY tanımlı değil")`; `EMAIL_FROM` yok → prod'da (kaçış bayrağı yoksa) `throw`, aksi hâlde mevcut `resend.dev`. `send`'e `replyTo: process.env.EMAIL_REPLY_TO ?? "destek@kolaytercih.com"` (Resend 6 alan adı `replyTo`).
2. `auth.ts` `sendMagicLinkEmail`: `:33-37` bloğu → `if (epostaKonsolModu()) { console.log(link); return; }`; aksi hâlde `epostaGonder` (yanlış yapılandırmada atar → better-auth 500 → formda hata toast'ı; P2 `giris_hata{status:500}` yazar). `__sonMagicLink` satırı yerinde kalır.
3. Soğuma — `auth.ts`, **yalnız `NODE_ENV === "production"`** (dev paneli "anında giriş" art arda istek atıyor): `globalThis.__girisEpostaSon: Map<string, number>`; anahtar `email.trim().toLowerCase()`; her çağrıda 60 sn'den eski kayıtları buda; kayıt varsa **sessizce `return`** (mail yok, istek 200); yoksa zamanı yaz → gönder; gönderim atarsa kaydı **sil** (kullanıcı hemen yeniden deneyebilsin) ve hatayı yeniden fırlat. Konsol modunda da çalışır (lokal docker testi görsün).
4. `eposta.ts`: `:79-80` iki `<link>` satırı silinir; footer'a (`:98-99`) üçüncü satır: `<a href="https://kolaytercih.com" style="color:#94a3b8;">kolaytercih.com</a> &middot; <a href="mailto:destek@kolaytercih.com" style="color:#94a3b8;">destek@kolaytercih.com</a>`; düz metin (`text`) gövdelerinin sonuna aynı iki bilgi. Satış dili ekleme.
5. `analitik-sunucu.ts:17`: `"odeme_tamamlandi" | "giris_basarili"`. `auth.ts` `databaseHooks`'a `session: { create: { after: async (session, ctx) => { void sunucuOlayi("giris_basarili", { kullaniciId: session.userId, pathname: "/giris", ozellikler: { yontem: ctx?.path?.startsWith("/magic-link") ? "eposta" : ctx?.path?.startsWith("/callback") ? "google" : "diger" } }); } } }` — **`await` yok**; imza `(session, context | null)` 1.6.23 tiplerinde doğrulandı (`@better-auth/core … init-options.d.mts`). E-posta özelliğe girmez.
6. **İsteğe bağlı commit [BİLAL]** `.gitea/workflows/ci.yaml`: `.env.production` bloğuna `RYBBIT_HOST`, `RYBBIT_SITE_ID`, `RYBBIT_API_KEY`, `EMAIL_REPLY_TO` satırları (secret yoksa boş gelir → kod no-op/varsayılan; zararsız). Gerekçe aşağıda, itiraz #4.
**Commit bölünmesi:** (1) `chore(eposta): şablondan harici font + footer iletişim` · (2) `fix(eposta): Reply-To + prod'da sessiz fallback yok` · (3) `feat(giris): adres başına 60 sn soğuma` · (4) `feat(analitik): giris_basarili sunucu olayı` · (5) [BİLAL] ci.yaml. Tamamı "Bilal görmeden merge edilmez"; (2) ve (3) canlı giriş yolunu değiştirir, tek tek atılabilir.
**Kabul kriterleri:** typegen+tsc+lint. `grep -n "fonts.googleapis\|fonts.gstatic" src/lib/eposta.ts` → boş. `grep -n "process.env" src/lib/eposta-gonder.ts` → tüm okumalar fonksiyon gövdesinde. `grep -n "await sunucuOlayi" src/lib/auth.ts` → boş. Kod incelemesi: prod + anahtarsız + bayraksız yolda hiçbir `console.log` URL basmıyor. Bilal'in merge öncesi lokal docker testi: `EPOSTA_KONSOL_FALLBACK=1` ile link konsola düşer, 60 sn içinde ikinci istek link **üretmez**; bayraksız/anahtarsız → formda hata.
**Kapsam dışı:** disposable filtre, vazgeçme linki/`List-Unsubscribe`, eski token iptali, Resend webhook, Traefik IP başlığı / better-auth `rateLimit` ayarı, DNS, `kredi-hatirlatma.ts`.
**Riskler:** Hata = kimse giriş yapamaz → (2)-(3) için lokal docker testi şart (bellek notu "testler lokal docker'la"). Süreç içi `Map` yeniden başlatmada sıfırlanır ve çoklu kopyada paylaşılmaz — bugün tek konteyner (`docker-compose.yml`), ikinci katman IP limiti duruyor. Soğumada düşen isteğin `verification` satırı yetim kalır (15 dk'da ölür, hiç postalanmadı — zararsız). `session.create` Google girişinde ve dev panelde de tetiklenir (dev'de env yok → no-op).
---
## 6. CTO itirazları (CEO kararına)
1. **P3 kabul kriteri eksik ve grep'i yetersiz.** Yalnız `riskNotuUret` düzeltilirse girişsiz /sonuc tadımlığı (LLM üretimi, DB'de kayıtlı) ve danışman sohbeti "ihtimal" diliyle kalır; prompt'lar bu dili **emrediyor**. Alternatif: commit 1b (okuma anında deterministik risk notu + iki prompt satırı). Prod'da `pnpm tadimlik --force` gerekmez.
2. **P4-3 "kaynak: `verification` tablosu" kırılgan.** `identifier` = token (`magic-link/index.mjs:61-68`), e-posta yalnız `value` JSON'unda (indeks yok, `name` ile biçim değişir), satır `sendMagicLink`'ten **önce** yazılıyor (kendine takılır), düşürülen her istek yeni satır bırakıp pencereyi kaydırır; düzeltmesi tabloya `DELETE` gerektirir. Alternatif: süreç içi `Map` — şema yok, DB yazımı yok, better-auth iç yapısına bağımlılık yok.
3. **P4-2 "net hata" modül seviyesinde atılamaz** — `next build` kırılır; yalnız gönderim anında.
4. **P4-5 `giris_basarili` prod'da büyük olasılıkla hiç düşmeyecek:** `sunucuOlayi` `RYBBIT_HOST/SITE_ID/API_KEY` ister (`analitik-sunucu.ts:13-15,31`); CI'ın yazdığı `.env.production`'da bu üçü **yok** (`.gitea/workflows/ci.yaml:17-34`). Rybbit'teki 3 `odeme_tamamlandi` kaydının kaynağı bu yüzden şüpheli (lokal docker testi olabilir). Bilal: Gitea secret'larını ekle + P4 commit 5'i al; yoksa huninin `giris_basarili` adımı boş kalır.
5. **P2 `paket_goruntulendi {kaynak}` CEO'nun dosya listesiyle yapılamaz** (`/paket` sayfası ve `paket-satinal.tsx` P3'ün; `SatinAlForm` yalnız girişliye render oluyor). Alternatif: yeni `paket/layout.tsx`. Ana sayfadaki iki `/paket` linki (`page.tsx:535,565`, P3'ün) kaynak taşıyamaz → `diger`.
6. **P2'ye erken-olay kuyruğu eklenmezse** yeni görüntülenme olayları sert yüklemede (OAuth dönüşü, e-postadan gelen, yer imi) sistematik eksik sayar; CEO listesinde yoktu, eklendi.
7. **İstemci 30 sn ↔ sunucu 60 sn:** kullanıcı 30. sn'de "Yeniden gönder"e basınca sunucu sessizce yutar, kullanıcı mail bekler. P2 geri sayımı 60'a çeker.
8. **"Her paket `pnpm build`"**: dört paralel build ≈ 8 GB (bellek notu: build ~2 GB). Yalnız P1 build eder; diğerleri typegen+tsc+lint; build Tur 4'te sırayla.
## 7. Backlog (bu gece yapılmayan teknik maddeler, öncelik sırasıyla)
1. **Prod tadımlık havuzu kontrolü** (`pnpm tadimlik`) — P2 yayına girip `tadimlik_goruntulendi` hâlâ 0 ise ilk şüpheli. [Bilal]
2. **Gitea secret'ları:** `RYBBIT_*`, `EMAIL_REPLY_TO` (itiraz #4). [Bilal]
3. Garanti→Güvenli (H2) tek paket: ~10 dosya + `SEMA` + aria + figcaption aynı commit; `cta-sira-form.tsx` "garanti dilimleri / nereye yerleşirsin" ve `page.tsx:132` dahil. Karar bekliyor.
4. Rehber T4 (`kume`/`ilgili` + `/rehber` küme başlıkları, `SectionEyebrow` ile) — P1 merge'inden sonra, aynı dosyalar.
5. `liste_uretim_hata` alt nedeni: `rapor-actions.ts` sonuç tipine `neden: "ai" | "dogrulama" | "tekrar"`; ardından `odeme_hata`, `sihirbaz_kapatildi`, `rapor_yazdirildi`; kalan `/paket` linklerine `kaynak` (`page.tsx`, `sohbet-client.tsx`, `tercih-degisti-modali.tsx`).
6. Tarih biçimi tekilleştirme: `veri-kaynagi.tsx`'in `toLocaleDateString`'i P1'in `trTarih`'ine geçsin (P1+P3 merge sonrası); hydration #418 şüphesini de kapatır.
7. better-auth `rateLimit` + `advanced.ipAddress.ipAddressHeaders` Traefik'e göre sabitlenmesi (IP başlığı yanlışsa limit ya herkese ortak ya hiç işlemiyor) — canlı ayar, lokal docker + Bilal.
8. `error.tsx`'e ChunkLoadError için tek seferlik reload; `/sonuc` pageview şişmesi (`replaceState`).
9. `el-cizimi-sema.tsx:14-15` font lisansı — alınmadıysa font değişimi işi.
10. Eski token iptali, Resend webhook/alarm, hatırlatma retry, disposable filtre (ürün/hukuk kararı sonrası).