# Uçtan uca denetim — 03.08.2026

*Production öncesi bağımsız denetim. Talep: Sets. Kapsam: topoloji · rol/yetki · güvenlik · tasarım sadakati (45 mockup dosyasının tamamı) · doküman→kod · stub/ölü kod · veri bütünlüğü · production hazırlığı.*

⚠ **Bu belge bulgu listesidir, plan değildir.** Düzeltme sırası ayrıca kararlaştırılacak (Sets, 03.08.2026: *"önce uçtan uca tüm bulguları netleştirelim, ondan sonrasında yapılacakları planlarız birlikte"*).

> ⛔ **BU BELGE 03.08.2026'NIN ANLIK GÖRÜNTÜSÜDÜR — ENVANTER DEĞİLDİR** *(not: 05.08.2026)*
>
> O tarihten beri Kuşak 0–4 kapandı ve bulguların büyük kısmı artık geçersiz. Ölçüldüğünde: §5.1'in "hiç yazılmamış yirmi ekranı"nın **on sekizi yazılmıştı**, §13.4'ün on sidebar bulgusunun **yedisi** kapalıydı. Tersi de oldu: §11'in "96 indekssiz yabancı anahtarı" **137** çıktı, "5 RLS tablosu" **9**.
>
> **Bir bulguyu kapatmadan önce koda sor.** Aksi hâlde ya yapılmış iş tekrar yapılır, ya da "kapandı" denen şeye hiç bakılmamış olur.
>
> Güncel durum ve kalan işler: `proje-haritasi.md` §2.

**Yöntem notu:** her bulgu ilgili dosya açılarak doğrulandı. Alt denetimlerin raporu tek başına kanıt sayılmadı; aşağıdaki `⬛ DOĞRULANDI` işaretli satırlar ana denetçi tarafından bağımsız olarak koda/vendor kaynağına inilerek teyit edildi. Alt denetimden gelip henüz bağımsız teyit edilmemiş satırlar `◻ RAPOR` ile işaretli.

---

## 0 · Otuz saniyelik özet

Ürün "ayakta görünüyor" — 1655 test yeşil, CI #886 SUCCESS. Denetim bu tablonun **üç kör noktası** olduğunu gösteriyor:

1. **Tarayıcıda hiç açılmamış kod.** Testler HTTP seviyesinde koşuyor; Vue bileşeni çözülemese, prop bağlanmasa, route hiç var olmasa da yeşil kalıyor. Bu sınıftan 10+ kırık bulundu.
2. **Kod var, ekran var, giriş noktası yok.** Yazılmış ve çalışan ekranlara hiçbir menüden/butondan ulaşılamıyor.
3. **Yetki, kapının kendisinde değil içeriğinde.** Birkaç ekran "kartlar kendini filtreler" gerekçesiyle route'ta korumasız bırakılmış; en az birinde filtre gerçekten yok.

Ayrıca **production altyapısı yok** (deploy script'i, rollback, prod nginx, prod env şablonu) ve **hedef topolojiden bir host eksik** (`panel.`).

---

## 1 · ÇIKIŞI ENGELLEYENLER

### 1.1 · Veritabanı portları tüm arayüzlere açık ⬛ DOĞRULANDI

`deployment/docker-compose.postgresql.yml:38-39` → `ports: - "${DB_DIRECT_PORT:-5534}:5432"`
`deployment/docker-compose.pgbouncer.yml:24-25` → `ports: - "${DB_PORT:-6433}:6432"`

İkisinde de `127.0.0.1:` öneki **yok** ve ikisi için de prod override dosyası **yok** (`deployment/` altında yalnız `docker-compose.redis.prod.yml` var).

Ek olarak `deployment/pgbouncer/pgbouncer.ini:18,23` → `listen_addr = 0.0.0.0`, `auth_type = plain`.

**Sonuç:** public IP'li bir sunucuda PostgreSQL 5534 ve PgBouncer 6433 portunda **internetten dinler**. Docker'ın port publish'i `ufw`/`iptables` INPUT zincirini atlar — sunucu güvenlik duvarı kapalı görünse bile port dışarıya açıktır. `DB_ADMIN_PASSWORD` ile superuser'a doğrudan kaba kuvvet mümkün.

⚠ Redis için bu doğru yapılmış (`docker-compose.redis.prod.yml:21-22,32` → `network_mode: host` + `--bind 127.0.0.1` + `ports: !override []` + firewall notu). Asimetri bilinçli değil, eksik.

### 1.1b · Horizon panosu production'da hiç kimseye açılmıyor ⬛ DOĞRULANDI — *önceki tespit düzeltildi*

⛔ **Düzeltme:** bu denetimin ilk turunda "Horizon production'da herkese açık" diye raporlandı. **Yanlış.** Yön terstir.

`vendor/laravel/horizon/src/Http/Controllers/Controller.php:15-18` — Horizon'un **taban controller'ı** kurucusunda `Authenticate` middleware'ini uyguluyor; tüm Horizon controller'ları bundan türüyor. Yani `Horizon::check()` her istekte koşuyor.

`Horizon::auth()` kod tabanında hiç çağrılmamış, `HorizonServiceProvider` yok → varsayılan geri çağırım devrede (`vendor/laravel/horizon/src/Horizon.php:81-85`):
```php
return app()->environment('local');
```

**Sonuç:**
- **Production'da:** `environment('local')` false → `ForbiddenException` → **403, herkes için.** Süper admin dahil hiç kimse Horizon'a giremez. Kuyruk gözlemi, failed job listesi, retry düğmesi erişilemez. Bu bir güvenlik açığı değil, **işletilebilirlik engeli.**
- **Local'de:** true → kimlik doğrulamasız açık. `EnsureBackofficeHost` host'u kısıtlıyor, yani yalnız geliştirme makinesinde ve yalnız backoffice host'unda.

Ciddiyet: **YÜKSEK (işletilebilirlik)**, kritik güvenlik değil. Kapanış §4'teki audit mühürleme bulgusuyla birleşince ağırlaşıyor: mühürleme işi düşerse ne bildirim gider ne panele bakılabilir.

### 1.2 · Production deploy altyapısı yok ⬛ DOĞRULANDI

`deployment/scripts/` içeriği: `stack.sh` (lokal docker), `teamcity.sh`, `youtrack.sh`, `heyet.sh`, `setup-test-database.sh`, `anonymize-dump.sh`.

Yok olanlar: deploy script'i, rollback yolu, yedekleme yapılandırması, bakım modu akışı.

### 1.3 · Production nginx yapılandırması yok ⬛ DOĞRULANDI

`deployment/nginx/` altında tek dosya: `egitim.dev.sets.fyi.conf`. `server_name` dev hostname'leri (`:53,70`), sertifika yolu `/opt/homebrew/etc/nginx/ssl/...` (`:72-73`) — geliştirme makinesi conf'u.

Güvenlik başlığı yok: tüm dosyada tek `add_header` var, o da statik dosya cache'i (`:98`). HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy — hiçbiri yok.

### 1.4 · `panel.` subdomain'i hiçbir katmanda yok ⬛ DOĞRULANDI

Hedef topoloji *(Sets, 03.08.2026 — `proje-haritasi.md` §5)*: düz domain = landing + panele giriş; `panel.<domain>` = müşteri paneli; `ofis.` ; `backoffice.`

Gerçek: `config/app.php:108-112` üç host tanımlıyor (`public`, `office`, `backoffice`). Müşteri paneli ayrı host değil, public host altında `/portal/*` path'i. `.env.example:29-31` ve nginx conf'unda da `panel.` geçmiyor.

`/panel` adresi ise stub: `CustomerPanelController.php:34-41` → `response()->json(['panel' => true])`.

### 1.5 · Landing page yok, müşteri girişi kırık ⬛ DOĞRULANDI

- `routes/web.php:44` → `Route::get('/', fn () => response()->noContent())` — kök adres boş 204.
- `routes/web.php:79` yalnız `POST /giris` tanımlıyor; **`GET /giris` yok.**
- `bootstrap/app.php:60` → `redirectGuestsTo(fn () => '/giris')`.

**Sonuç:** `/portal`'a giden misafir `/giris`'e yönlendiriliyor, URI eşleşiyor ama metot eşleşmiyor → **405**. Müşteri panelinin giriş kapısı hiç açılmıyor. Tasarım `10`'un çizdiği müşteri giriş ekranı (desktop + 375px), passcode/SMS OTP kartı, "bağlantı gönderildi" ve "süresi doldu" halleri de hiç yazılmamış.

✅ **KAPANDI — 03.08.2026.** Landing page (`resources/views/landing.blade.php`) + `panel.` host'u + `routes/panel.php` (madde 0.3); `GET /giris`, magic link "gönderildi"/"süresi doldu" halleri, kimlik teyidi ekranı, ikinci faktör adımı ve şifre sıfırlamanın iki kartı (madde 0.4). SMS passcode kartı **kapsam dışı** — 27.07.2026'da (Sets) dördüncü yöntemin passkey olduğu, gönderilen kodun magic link olduğu kayda geçmişti. Kapsama matrisi ve açık kalan dört soru: `docs/reviews/tasarim-10-musteri-kapsama.md`.

⛔ **Aynı turda çıkan üç yeni bulgu** (matris §8): (1) müşteri ikinci faktörü **hiç sorulmuyordu** — `hasCompletedMfa()` yalnız kaydı okuyor, `customer.approve_quote` MFA kapısı böyle geçiliyordu, düzeltildi; (2) `two_factor_recovery_codes` cast'siz — yazarken JSON, okurken ham string, düzeltildi; (3) müşteri girişinde **hız sınırı yok** (`ThrottlesSignIn` kullanılmıyor) — ofis tarafında var, **açık**.

---

## 2 · GÜVENLİK — kırmızı çizgi ihlalleri

### 2.1 · Filament'te üç kaynak yetkisiz açık ⬛ DOĞRULANDI

`app/Filament/Backoffice/Resources/` altında 16 kaynak var. `Groups`, `CustomerSegments`, `SurveyQuestions` üçlüsünün **policy dosyası yok** (`app/Policies/` listesinde `GroupPolicy`, `CustomerSegmentPolicy`, `SurveyQuestionPolicy` bulunmuyor; diğer 13'ünkü var).

Filament'te strict authorization açılmamış → policy yoksa varsayılan **allow**. `Gate::before` de argümanlı çağrılarda `null` dönüyor (`AuthServiceProvider.php:191-196`), yani süper-admin baypası da bunu kapatmıyor.

**Sonuç:** herhangi bir backoffice hesabı bu üç kaynakta viewAny/create/update/delete yapabiliyor. `Groups` ağır olanı: ofis yetki erişim ağacı (`reaches()`) bu tabloya dayanıyor → **yetki yükseltme vektörü.**

Arch testi bunu yakalamıyor çünkü `refuseATenantModelWithoutAPolicy` yalnız `BelongsToTenant` kullanan modelleri denetliyor; bu üçü sistem tablosu.

**Kırmızı çizgi #3.**

### 2.2 · Kullanım logu (AD-21) hiç yazılmıyor ⬛ DOĞRULANDI

`config/audit.php:169-174` desenleri: `ofis.*`, `panel.*`, `katilimci.*`, `backoffice.*`
Gerçek route adları: `office.*` (193 adet), `customer.*` (65 adet), `mail.*` (2 adet).

`LogUsage.php:63-64` `Str::is($pattern, $name)` ile eşleştiriyor → **sıfır eşleşme.**

Audit'in üçüncü sınıfı fiilen ölü. Testler middleware'i atlayıp `UsageLogger::record('ofis.teklifler.index', …)`'i doğrudan çağırdığı için yeşil kalmış — kablolama uçtan uca hiç test edilmemiş.

**Kırmızı çizgi #13.**

### 2.3 · Magic link kimlik teyidi sınırsız denenebiliyor ⬛ DOĞRULANDI

`routes/web.php:118` `POST /kimlik-teyidi` — throttle middleware yok. `AnswerMagicLinkChallenge.php` içinde RateLimiter / deneme sayacı / kilit / başarısızlık audit'i yok (grep → sıfır).

Cevap TCKN'nin son 4 hanesi → 10.000'lik uzay. Saldırganın elinde o noktada zaten yarı açık bir müşteri oturumu var (`ConsumeMagicLink.php`). Ortalama ~5.000 istekte "posta kutusunu okudu" → "o kişi oldu"ya dönüşüyor.

Ofis tarafındaki muadili (`AnswerSecondFactorChallenge.php:36,45-52`) tam bu tehdidi anlatıp koruyor; müşteri tarafına taşınmamış.

### 2.4 · Operasyon gündemi yetkisiz ◻ RAPOR

`AgendaController.php:90` hiçbir yetki kontrolü yapmıyor. `OperationsAgenda.php:180-215` tüm müşterilerin vadesi geçmiş fatura tutarlarını müşteri adıyla döndürüyor; `:81` tüm eğitmenlerin belge/expiry listesi, `:121` kişi başına açık/geciken görev sayısı — üçü de kapsamsız.

Aynı veri `AccountingController.php:47`'de `financial.view` arkasında. Docblock (`AgendaController.php:85-88`) *"her kart kendini filtreler"* diyor; dört karttan yalnız `approvals` filtreliyor.

`invoices` RLS'ten muaf (`config/tenancy.php`) — koruma yalnız query tarafında, ve bu query'de filtre yok.

### 2.5 · `/yonetim` ve `/onaylar` sıfır yetkiliye açık, menü ile route ayrışmış ◻ RAPOR

| Ekran | Menü izni | Route izni |
|---|---|---|
| `/yonetim` | `anyOf(expense.approve, training.approve, training.cancel_approve, discount.approve)` (`OfficeNavigation.php:157-159`) | **yok** (`AgendaController.php:90`) |
| `/onaylar` | `expense.approve` (`OfficeNavigation.php:181`) | **yok** (`ApprovalController.php:58`) |

İki yönlü hata: menüde gizlenen ekran adres yazılarak açılıyor; ters yönde `training.approve` tutup `expense.approve` tutmayan bir direktöre onay kuyruğu menüde hiç gösterilmiyor — oysa kuyruk dört tür taşıyor (masraf, belge, maliyet eşiği, iptal).

### 2.6 · `POST /planlama/sorgulama/iletisim` yetkisiz ◻ RAPOR

`StaffQueryController.php:104-112` — aynı controller'ın `index:56`, `export:127`, `saveView:167`, `deleteView:176` metotlarının dördü de `authorize('viewAny', Training::class)` çağırıyor; bu çağırmıyor. Aksiyondaki `#[NoAuthorizationRequired]` gerekçesi *"ekranı okumak korunan eylemdir"* — endpoint o ekranı okumuyor, doğrudan POST edilebiliyor.

Etki: yetkisiz ofis kullanıcısı, gönderdiği 100 keyfi müşteri id'si için satışçılara görev açtırabiliyor.

### 2.7 · Dosya yüklemede tip doğrulaması yok ⬛ DOĞRULANDI (kısmi düzeltmeyle)

`UploadParticipantDocumentRequest.php:29` → `['required', 'file', 'max:10240']`.

Boyut sınırı **var** ama koda gömülü (10 MB). Eksik olan: uzantı/MIME kısıtı (`mimes:`) ve `document_types.allowed_extensions` / `max_file_size_kb` kolonlarının sunucuda hiç okunmaması — bunlar yalnız tarayıcıda uygulanıyor (`Portal/People.vue:486-487`). `.svg` / `.html` yüklenebilir.

Aynı desen: `UploadInstructorDocument.php`, `UploadSupplierDocument.php`.

### 2.8 · Trusted proxy yapılandırılmamış ◻ RAPOR

`bootstrap/app.php` içinde `trustProxies()` çağrısı yok. `request()->ip()` üç kritik yerde kullanılıyor:

- throttle IP bütçesi (`ThrottlesSignIn.php:122`, `AnonymousRequestLimiter.php:34`)
- audit / erişim logu IP alanı (`AuditWriter.php:59`, `AccessLogger.php:50,92`)
- **KVKK rıza kanıtı** (`ParticipantAreaController.php:109` → `GiveParticipantConsent.php:70`)

Nginx arkasında hepsi proxy IP'sini kaydeder: IP bütçesi tüm kullanıcılar için tek sayaca çöker, rıza kaydındaki IP delil değeri taşımaz.

### 2.9 · Müşteri girişinde hız sınırlaması yok ◻ RAPOR

`routes/web.php:79` → `SignInCustomerUser.php:39`. `config/security.php` `login_throttle` yalnız `ThrottlesSignIn` tarafından okunuyor; müşteri tarafında hiçbir kod onu inject etmiyor. `docs/issues.md` bunu zaten bilinen açık olarak kaydediyor.

### 2.10 · Kritik kayıtlarda hard delete koruması eksik ◻ RAPOR

| Ne | Kanıt |
|---|---|
| `ContentPackageDocument` korumasız `DeleteAction` — `content_access_events` `cascadeOnDelete` ile bağlı → kim görüntüledi/indirdi + watermark token izi sessizce yok oluyor | `DocumentsRelationManager.php:113` + `2026_09_02_000200:167-171` |
| `RegulationDocument` hard delete — resmî mevzuat dosyası, `SoftDeletes` yok, trigger yok | `Regulations/RelationManagers/DocumentsRelationManager.php:209` |
| `exam_results`, `certificate_records`, `training_completions` — `BEFORE DELETE` deny trigger'ı yok; `TenantPolicy::statements()` bunlara `FOR DELETE` policy'si üretiyor | `2026_08_30_000100:209,252,284` |
| `quotes`, `quote_versions`, `quote_approvals`, `quote_items` — ne RLS ne deny trigger | `2026_09_01_000100_create_quotes.php` |

**Kırmızı çizgi #12.**

### 2.11 · Portal okuma ekranlarında izin ayrımı yok ◻ RAPOR

`PanelAccess::isOpenTo` (`app/Queries/Customer/PanelAccess.php:47-56`) **herhangi bir** müşteri izni varsa true dönüyor. `/portal/organizasyon`, `/portal/personel`, `/portal/lokasyonlar`, `/portal/icerik`, `/portal/sertifikalar` yalnız bu kapıyı kullanıyor.

Yalnız `customer.view_invoices` tutan bir kullanıcı organizasyon ağacını, personel listesini ve belge kütüphanesini okuyabiliyor. Tenant sınırı korunuyor, şirket içi ayrım korunmuyor.

### 2.12 · Temiz çıkanlar (kanıtlı)

Tenant context `SET LOCAL` disiplini (AD-04) · `DB::table()` çağrılarının tamamı `TenantContextGuard` denetiminde · SQL enjeksiyonu · XSS (`v-html` tek kullanım, kaynağı statik çeviri) · CSRF (iki istisna, ikisi de `hash_equals` ile secret doğruluyor) · açık yönlendirme · mass assignment · sabit-zamanlı karşılaştırmalar · `env()` disiplini (config dışında yok) · kod yerleşimi AD-25 · audit yazımının aynı transaction'da senkron olması.

---

## 3 · TARAYICIDA HİÇ AÇILMAMIŞ KOD — sessiz kırıklar

Bu sınıfın ortak özelliği: 1655 testin hiçbiri bu kod yollarını tarayıcıda render etmiyor.

✅ **ALTISI DA KAPANDI (03.08.2026)** — ve kapatılma şekli bu bölümün kendi dersini doğruladı. Tarayıcı turu kuruldu (`docs/reviews/tarayici-turu-bulgulari.md`); K-1 ve K-6 turun bulduğu 274 tanımsız sınıfla birlikte kapandı, arch testi yazıldı.

⛔ **K-2'nin kapanışı ayrıca kayda değer:** turun ilk hâli `/muhasebe`'yi "temiz" raporladı — çünkü *"Failed to resolve component"* bir **geliştirme uyarısıdır** ve `npm run build` onu atar. Tur, insanların kullandığı derlenmiş uygulamayı okuduğu için uyarıyı değil **cesedi** aramak zorunda: çözülemeyen bileşen DOM'da `HTMLUnknownElement` olarak kalıyor. Kontrol eklendi; `<chartcard>`, `<lineareachart>` ve `<donutchart>` oradaydı.

⚠ K-5 için ayrıca arch testi yazıldı: **bileşenin tanımlamadığı bir `v-model:` adına bağlanmak** artık derlemeyi kırar. Vue bunu sessizce kabul ediyor — bağlama `$attrs`'a düşüyor, bileşen görmüyor, alan hiçbir şey yapmıyor.

| # | Kırık | Kanıt | Etki |
|---|---|---|---|
| K-1 ✅ | **`--color-ink-500` tanımlı değil** ama `text-ink-500` 66 dosyada kullanılıyor | `tokens.css:15-22` ink skalası 950/800/600/400/300/200/100/50; `grep -rl ink-500 resources/js` → 66 | Hiç CSS üretmiyor; `ChartCard`, `FileDrop`, `MultiSelect`, `CascadeSelect` gibi **ortak bileşenlerin** içinde → hata her ekrana yayılıyor. Aynı durum `border-line`, `text-link`, `text-ink-700`, `text-ink-900`, `text-warning-700` için ⬛ DOĞRULANDI |
| K-2 ✅ | **Muhasebe ekranının iki grafiği hiç render olmuyor** | `Accounting/Overview.vue:152-193` şablonda `ChartCard`/`LineAreaChart`/`BarChart`/`DonutChart`; `script setup` (`:15-18`) hiçbirini import etmiyor; `office.js` içinde global kayıt yok | İki grafik kartı boş çizilir, konsola "Failed to resolve component". `current_todo.md:69`'daki teslim iddiasını yalanlıyor ⬛ DOĞRULANDI |
| K-3 ✅ | **`/egitimler/planla` route'u yok** | `routes/office.php` içinde tanımsız; çağıranlar `Planning/Query.vue:225` ve `Agenda/Planning.vue:199` | Planlamacı gündeminin birincil butonu ("+ Eğitim Planla") ve sorgulamanın çıkış eylemi 404 ⬛ DOĞRULANDI |
| K-4 ✅ | **`Plan.vue:531` tanımsız değişken** | `aday_il: form.city_option_value_id` — dosyada `form` yok, reaktif nesnenin adı `plan` (`:93`) | "Aday listesini aç" tıklandığı anda `ReferenceError`. Tasarım `24`'ün eğitmen atama kapısının tamamı erişilemez ⬛ DOĞRULANDI |
| K-5 ✅ | **`Requests/Create.vue:163-164` sessiz bağlanmama** | `v-model:from` / `v-model:until` yazılmış; `DateRangePicker.vue:62-63` `start`/`end` bekliyor | Zorunlu işaretli "Tercih edilen aralık" her zaman boş gider. Diğer üç çağıran doğru — sapma tek dosyada ⬛ DOĞRULANDI |
| K-6 ✅ | Arch testi tanımsız token'ı yakalamıyor | `FrontendTest.php:42-83` yalnız token'ların *tanımlı* olduğunu doğruluyor; kullanılan `-{token}` utility'sinin karşılığı olup olmadığını kimse denetlemiyor | Hex ve arbitrary değer kapatılmış, tanımsız isim açık kalmış ◻ RAPOR |

---

## 4 · KOD VAR, EKRAN VAR, GİRİŞ NOKTASI YOK

| # | Ne | Kanıt |
|---|---|---|
| G-1 | **Teklif PDF'i** — `Quotes/Printout.vue` tam yazılmış (tasarım `37`), route var | `routes/office.php:460`; `grep "yazdir" resources/js` → **sıfır**. Ne satışçı basabiliyor ne müşteri indirebiliyor — ama `Portal/QuoteShow.vue:257-288` müşteriden **imzalı nüshayı yüklemesini** istiyor. Kapalı döngü ⬛ DOĞRULANDI |
| G-2 | **İçerik paketi kataloğu** — iki ekran yazılmış | `routes/office.php:450,452` var; `OfficeNavigation.php` içinde "paket/package" geçmiyor ⬛ DOĞRULANDI |
| G-3 | **Portal eğitim detay sayfası** | `routes/web.php:310` + `TrainingShow.vue` var; `Trainings.vue:103-131` satırlarında link yok, `Dashboard.vue` yaklaşan eğitimlerde de yok ◻ RAPOR |
| G-4 | **Planlamacının talep kutusu** `/planlama/talepler` | Route + yetki kapısı var (`routes/office.php:393`), menü maddesi yok ◻ RAPOR |
| G-5 | **`/satis/antet`** teklif antedi | Menüde yok ◻ RAPOR |
| G-6 | **Maliyet "gerçekleşen" girişi** | `POST /egitimler/{t}/maliyet/{item}/gerceklesen` (`routes/office.php:621`) — `resources/js` içinde çağıran yok ◻ RAPOR |
| G-7 | **Fatura kesme** | `POST /muhasebe/faturalar` (`routes/office.php:522`) — çağıran yok; `Overview.vue:76` butonu yalnız listeye götürüyor, listede oluşturma kontrolü yok ◻ RAPOR |
| G-8 | **Talep yeniden açma** | `PATCH /planlama/talepler/{r}/yeniden-ac` (`routes/office.php:397`) — çağıran yok ◻ RAPOR |
| G-9 | **Eğitmen için yoklama listesi yazdırma** | `routes/office.php:642` yalnız `Trainings/Show.vue:319`'dan erişilebiliyor; eğitmen kendi sahada imzalatacağı listeyi basamıyor ◻ RAPOR |

---

## 5 · TASARIM SADAKATİ

45 mockup dosyasının tamamı okundu. Tam kapsama matrisleri alt denetim raporlarında; burada yalnız **hiç yazılmamış ekranlar** ve **en ağır kompozisyon sapmaları**.

### 5.1 · Hiç yazılmamış ekranlar

| Tasarım | Ekran | Not |
|---|---|---|
| `10:64-113` | **Müşteri portal girişi** (desktop + 375px) | §1.5 |
| `10:133-159` | Müşteri passcode (SMS OTP) · "bağlantı gönderildi" · "süresi doldu" | — |
| `10:165-191` | Müşteri şifre sıfırlama | `routes/web.php:112` → `noContent()` stub |
| `10:192-217` | İlk giriş KVKK aydınlatma + açık rıza (scroll-to-accept) | Katılımcı panelinde scroll kilidi olmayan kart var |
| `10:266,268` | Profil "Bilgilerim" ve "Bildirim tercihleri" sekmeleri | Yalnız Güvenlik var |
| `16:142-152` | İçerik paketi PDF görüntüleyici paneli | — |
| `20:153-201` | **Planlama kuyruğu** (`/planlama/kuyruk`) — onaylı teklifler, SLA rozetleri, "Mevcut Eğitime Bağla", "Bölgeye Duyuru Aç" | Onaylı tekliften planlamaya giden kuyruk hiç yok |
| `22:130-161` | Maliyet kalemi düzenleme / "gerçekleşen" girişi | Route var (G-6) |
| `25:193,303` | Tedarikçi pasifleştirme + belge yükleme | Şema var (`suppliers.is_active`, `supplier_documents`), route ve ekran yok |
| `34:399-413` | Talep hatırlatma paneli + not ekleme | — |
| `36:264-272` | Operasyon gündemi "Bu hafta operasyon" kartı | — |
| `37:113-135` | **Antet / belge şablonları yönetimi** (`/yonetim/belge-sablonlari`) | Antet Filament `BusinessSettings.php:528` altında tek alan; sürümleme yok |
| `41:231-291` | Masraf girişi drawer'ı + 375px varyantı | Inline form ile değiştirilmiş |
| `41:195-227` | Eğitim günü 375px kart görünümü | Desktop tablosu mobilde yatay kaydırılıyor |
| `50:32-91` | **Katılımcı magic-link karşılama / kimlik teyidi** (`/k/{token}`) | Sunucu tarafı var (`AnswerMagicLinkChallenge`), Inertia sayfası yok — `MagicLinkController::show()` JSON döndürüyor |
| `50:125-128` | Katılımcı "Yer & program" kartı | — |
| `62:145-172` | Fatura oluşturma (Serbest Kalem / Teklifi Faturalandır) | Endpoint var (G-7) |
| `64:341-388` | **Yeni ofis kullanıcısı — 3 adımlı sayfa** | Drawer ile karşılanmış; adım göstergesi, "hesap sıfır yetkiyle doğar" bandı, üç başlangıç kartı yok |
| `66:382-392` | Sonuçlananlar / karar geçmişi | Verilmiş kararlar hiçbir ekranda listelenmiyor |
| `68:144-151` | "DİKKAT TONU — NE ZAMAN" açıklama kartı | 3'lü kart satırı 2'ye düşmüş |

### 5.2 · Katalog boşlukları (`00`–`06`)

**Hiç yazılmamış atomlar:** Rating yıldızı (`01:314` — `Portal/Survey.vue:146,201` içine ham `<button>★</button>` gömülü), Slider + limit işareti (`01:319-325`), Tooltip (`04:148-151` — yalnız native `title=""`), Popconfirm (`04:82-90`), Timeline (`02:220-226`), KPI kartı (`02:235-239`), kapasite/progress çubuğu (`02:243-245` — **9 sayfada kopyalanmış**), description list (`02:249-254` — **21 sayfada elle `<dl>`**), tablo/kart skeleton (`04:208-229`), mevzuat callout (`04:190-199`), tab şeridi (`05:124-130`), bildirim merkezi bileşeni (`05:136-172`).

**Eksik varyant/durum:** TextInput hover + loading; Checkbox indeterminate + hata; OTP deneme sayacı + geri sayım; FileDrop progress + yüklenmiş dosya satırı; Toast yalnız 2 ton (katalogda 3 + ✕ + aksiyon linki).

**Mobil navigasyon yok:** `05:66-78` ve HANDOFF kural 12 eğitmen/katılımcı panellerinde ≤5 hedefli bottom-nav şart koşuyor. `AppSidebar.vue:69` ve `PortalSidebar.vue:37` `lg:flex` — 1024px altında sidebar tamamen gizleniyor, yerine hiçbir şey konmamış. **375px'te eğitmen panelinde gezinme yok.**

**Mobil dokunma hedefi:** `AttendanceMark.vue` `large` prop'u var, `TrainingDay.vue:347` geçmiyor → sahada 22px hedef (tasarım 34px, HANDOFF kural 11 ≥44px).

**DataTable, `02` tablo standardının yarısı:** çoklu seçim + bulk bar, kayıtlı görünümler, Kompakt/Rahat yoğunluk, kolon seçici, tfoot toplam satırı, numaralı sayfalama — hiçbiri yok. Ayrıca **32 sayfa DataTable'ı hiç kullanmayıp ham `<table>` yazıyor.**

**A4 baskı kuralları yok:** repoda tek bir `@media print` / `@page` yok. `Trainings/Register.vue:44` `window.print()` çağırıyor ama 15mm kenar boşluğu, taşan sayfada başlık tekrarı, sayfa numarası tanımlı değil.

### 5.3 · Mevcut kapsama matrislerinin güvenilirliği

⛔ **`docs/reviews/tasarim-*-kapsama.md` dosyalarının bir kısmı ✓ verdiği maddeler kodda yokken kapatılmış.** Tespit edilen en az 8 satır:

- `tasarim-40-kapsama.md`: rail "GENEL" grubu (gerçekte `instructor` grubunda); kokpitte "Eğitim günü ekranı →" bağlantısı (`Dashboard.vue`'de hiç yok)
- `tasarim-41-kapsama.md`: `AttendanceMark large`; masrafta `camera` prop'u; "Tür çipleri" (gerçekte `SearchableSelect`); "Yoklama Listesi İndir (PDF)"
- `tasarim-38-kapsama.md:48-51`: B7 "gelen e-postadan görev oluştur" (arka uç hazır, UI'da hiç yok)
- `tasarim-62-kapsama.md`: grafik kartları (K-2 — render olmuyor)

**Sonuç: kapsama matrisi üretimi bu turdan itibaren bağımsız doğrulama ister; matrisin kendisi kanıt değildir.**

---

## 6 · TOPOLOJİ VE ROL/YETKİ

### 6.1 · Hedef vs gerçek

| Hedef | Gerçek | Durum |
|---|---|---|
| `backoffice.<domain>` → Filament backbone | Var, kendi host'unda | ✓ |
| `ofis.<domain>` → ofis paneli | Var, `routes/office.php` yalnız ofis host'una bağlı | ✓ |
| düz domain → landing page | **Yok** — `/` 204 dönüyor | ❌ |
| düz domain → müşteri paneline erişim | **Kırık** — `GET /giris` yok, misafir 405 alıyor | ❌ |
| `panel.<domain>` → müşteri paneli | **Yok** — portal düz domainde `/portal/*` | ❌ |

**Host sızması yok:** her iki route grubu da `->domain()` taşıyor, Filament paneli de öyle. Domainsiz 35 route'un tamamı framework/paket route'u; `storage/*` imzalı URL zorunlu, `filament/exports|imports/*` imza + sahiplik kontrollü.

### 6.2 · Operasyon direktörünün erişemediği ekranlar

`/egitmen/*` grubunun tamamı (17 route: panel, eğitimlerim, takvim, müsaitlik, masraflarım, cari durumum, belgelerim). Kapı izin değil, `Instructor` kaydının varlığı — kaydı olmayan **404** alıyor (`InstructorPanelController.php:604-611`).

Hedef *("backoffice'ten yetki verildiyse TÜM sayfa/panellere erişir")* bu grupta hiçbir izinle sağlanamıyor.

### 6.3 · Menü ↔ route

**Menüde olup route'u olmayan madde: yok.** 29 menü route adının 29'u da mevcut; çeviri anahtarları eksiksiz.

**Route'u olup menüde olmayan gerçek ekranlar:** `/planlama/talepler`, `/satis/paketler`, `/satis/antet` (G-2, G-4, G-5).

**Menü izni ≠ route izni:** §2.5'teki üç madde.

---

## 7 · HARDCODED İŞ KURALI (AD-08 / AD-27 / kırmızı çizgi 9, 14)

Omurga temiz: azami/asgari katılımcı, `min_pass_score`, `renewal_months`, bildirim gün sayıları `training_definitions` kolonlarından; `Settings` geniş bir parametre yüzeyi sunuyor. `app/Domain/**`, `app/Jobs/**`, `app/Console/Commands/**` temiz.

Kalan sabitler ◻ RAPOR:

| Sabit | Dosya:satır | Neden sorun |
|---|---|---|
| `addDays(60)` — eğitmen belgesi "yakında doluyor" ufku | `OfficeInstructors.php:104` | Diğer tüm tüketiciler `Settings::notificationWarningDays()`'ten türetiyor → **rozet 60 günde, görev 30 günde açılıyor**, ekran ile görev haftalarca ayrışıyor |
| `WARNING_SPANS = 6` (5×6=30 gün çarpanı), iki dosyada kopya | `MyCockpit.php:57`, `OpenInstructorDocumentRenewalTasks.php:55` | Taban admin-tanımlı, çarpan değil |
| `WINDOWS = [30,60,90,120]` — üç kopya (PHP sabiti + validation kuralı + Vue) | `SetComplianceWarningWindow.php:37`, `SetComplianceWindowRequest.php:22`, `Organisation.vue:41` | — |
| `SurveyResults::DAYS = 90` | `SurveyResults.php:45` | Eşleştiği `surveyMinimumResponses()` ayar; sınıf `Settings`'i zaten inject ediyor |
| `OVERDUE_TASK_DAYS = 30` + Vue'da `days >= 30` | `OperationsAgenda.php:38`, `Operations.vue:229` | Sunucu/istemci bağımsız kayabiliyor |
| Vue prop **default**'larında mevzuat sayıları: `ministryNotificationDays: 15`, `maxAuthorisedInstitutions: 2`, `documentTaskDays: 10`, `grace_months: 3`, `warning_days: 60`, `minimum_responses: 5`, `slaHours: 24`, `?? 0.5` düşük doluluk | `Instructors/Create.vue:53-55`, `Portal/People.vue:32`, `Portal/Certificates.vue:13`, `Agenda/Surveys.vue:20-21`, `Approvals/Index.vue:35-36`, `Trainings/Show.vue:106` | Sunucu ayarı doğru, ama regülasyon sayısı **kodda ikinci kez yazılı**. Anket anonimlik tabanının istemci fallback'i olması ayrıca sakıncalı |

---

## 8 · AUDIT BOŞLUKLARI (AD-06 / AD-16 / AD-21) ◻ RAPOR

| # | Bulgu | Kanıt |
|---|---|---|
| A-1 | Kullanım logu hiç yazılmıyor | §2.2 ⬛ |
| A-2 | `filters` ham query string, maskeleme yok → §2.2 düzeltilir düzeltilmez arama kutusuna girilen e-posta/TCKN `audit_usage.filters`'a maskesiz düşer | `LogUsage.php:79-83` |
| A-3 | `ResolveTasks` toplu `->update()` → model event yok → audit satırı yok | `ResolveTasks.php:40` |
| A-4 | Kimlik **mint eden/etkinleştiren** action'lar audit'siz: `EnrolAppAuthentication`, `IssueMagicLink`, `RequestMagicLink`, `RequestOfficePasswordReset`, `RequestCustomerPasswordReset`, `AcceptCustomerInvitation` | `app/Actions/Auth/*` — "2FA kapatmak kayıtlı, açmak kayıtsız" asimetrisi |
| A-5 | `ReadPackageDocument` şifresi çözülmüş baytları okuyor, `AccessLogger` çağırmıyor | `ReadPackageDocument.php:101-129` — tek kanıt cascade delete'e tabi iş tablosunda (§2.10) |
| A-6 | `ReadTrainingMaterial` okumayı `audit_writes`'a yazıyor, `audit_access`'e değil | `ReadTrainingMaterial.php:92` — sınav anahtarı içerebilen materyal |
| A-7 | Mevzuat belgesi `Storage::disk()->download()` ile doğrudan akıtılıyor — `SecureFileStore` baypas, erişim logu yok | `Regulations/RelationManagers/DocumentsRelationManager.php:198-199` |
| A-8 | Arch testi audit'i yalnız `BelongsToTenant` modellerinde zorluyor; `Quote`, `QuoteVersion`, `User`, `Instructor` muaf | `AuditTest.php:31-40` |

---

## 9 · PRODUCTION HAZIRLIĞI

| # | Bulgu | Kanıt | Ciddiyet |
|---|---|---|---|
| P-1 | Deploy/rollback/yedek/bakım modu yok | §1.2 | Çıkışı engeller ⬛ |
| P-2 | Production nginx conf'u yok | §1.3 | Çıkışı engeller ⬛ |
| P-3 | Güvenlik başlıkları yok (HSTS/CSP/X-Frame-Options/Referrer-Policy/Permissions-Policy) | `…conf:98` tek `add_header` | Yüksek ⬛ |
| P-4 | **Postmark anahtarı sürüklenmiş** — config `POSTMARK_API_KEY` okuyor, `.env.example` `POSTMARK_SERVER_API_TOKEN` tanımlıyor ve bunu hiçbir config okumuyor | `config/services.php:18` ↔ `.env.example:143`; `MAIL_MAILER=postmark` | İlk mail denemesinde patlar ⬛ |
| P-5 | `.env.example` bir geliştirme şablonu (`APP_ENV=local`, `APP_DEBUG=true`, `LOG_LEVEL=debug`); production şablonu yok | `.env.example:10,12,39` | Yüksek ⬛ |
| P-6 | `panel.` host'u yok | §1.4 | Çıkışı engeller ⬛ |
| P-7 | `session.secure` kod varsayılanı yok (`env('SESSION_SECURE_COOKIE')` → null → false) | `config/session.php:177` | Orta ◻ |
| P-8 | `DB_SSLMODE` varsayılanı `prefer` — sunucu TLS desteklemezse sessizce düz metne düşer | `config/database.php:115,135,159` | Düşük ◻ |
| P-9 | `public/fixtures/` derlenmiş tasarım SPA'i prod build'inde, tüm host'larda public | `public/fixtures/` | Düşük ◻ |

**Temiz çıkanlar ⬛:** 7 schedule komutunun 7'si de mevcut · seeder'lar `updateOrCreate` ile idempotent · closure route yok (`route:cache` uyumlu) · `SESSION_DOMAIN=null` → cookie host-bazlı, host'lar arası oturum sızmıyor.

### 9.1 · ⛔ Düzeltme — eğitim katalogu demo veri DEĞİL

Bu denetimin production turunda *"`DatabaseSeeder`'daki `TrainingDefinitionSeeder` 32 satır DEMO veri ve prod'a girerse 3E'nin onaylamadığı tanımlar katalogda belirir"* diye raporlandı (ve kullanıcıya iki kez böyle aktarıldı). **Yanlış.**

Seeder açılıp veritabanına karşı sayıldı (`TrainingDefinition::count()` → 32). Satırların tamamı resmî kaynaktan geliyor:

| Aile | Adet | Kaynak |
|---|---|---|
| DGY hizmet alanı | 9 | docx Ek A / DG-2020 Ek-3 |
| IMDG görev eğitimi | 13 | docx Ek B / YÖN-2019 Ek tablosu |
| TY-2019 kod eğitimi | 10 | TY-2019 Ek-5/2 |

Süreler, yenileme periyotları (görev 11 ve 12 için 60 ay dahil) ve kapsama kuralları dokümanla birebir tutuyor. Docblock'taki "DEMO DATA" ibaresi *"3E ekrandan kendi kataloğunu düzeltir"* anlamında; uydurma satır anlamında değil. Ayrıca `firstOrCreate` kullanıldığı için 3E'nin ekrandan yaptığı düzeltmeler yeniden koşuda korunuyor.

**Bu maddenin roadmap karşılığı iptaldir.** Katalogu "gerçeğiyle değiştirmek" 10 TY-2019 satırını silmek olurdu.

---

## 10 · VERİ BÜTÜNLÜĞÜ — akışı sessizce bozan kalemler

### 10.1 · Açık eğitimde katılımcı listesi kalıcı olarak boş ⬛ DOĞRULANDI *(alt denetimin iddiası daraltıldı)*

⛔ **Düzeltme:** alt denetim *"`training_customers` tablosuna hiçbir yerde yazılmıyor"* dedi. **Yanlış.** `app/Actions/Office/PlanTraining.php:135` yazıyor:
```php
if ($input->customerIds !== []) {
    $training->customers()->attach($input->customerIds);
}
```

Gerçek bulgu bundan **dar ama daha sinsi**:

- **Kapalı eğitim:** `PlanTraining.php:88-95` en az bir firma şart koşuyor → tablo dolu → her şey çalışıyor. ✓
- **Açık eğitim:** planlama anında firma yok, `customerIds` boş → attach hiç koşmuyor. Katılımcılar sonradan portalden veya ofisten yazılıyor, ama **ne `EnrolOwnParticipant` ne `EnrolParticipant` firmayı `training_customers`'a bağlıyor** (ikisinde de `attach`/`customers()` geçmiyor).

Sonuç: açık eğitimde `training_customers` **sonsuza kadar boş** kalıyor. Tenant kapsamını bu tablodan türeten **9 canlı okuyucu** boş küme alıyor:

`TrainingRoster.php:76` (katılımcı listesi) · `TrainingDayState.php:257` (eğitim günü ekranı) · `SurveyResults.php:85` (anket sonuçları) · `FindEnrolment.php:37` · `FindSuggestion.php:36` · `ApprovalEvidence.php:453` (onay kanıtı) · `FileTrainingRegister.php:135` (imzalı yoklama) · `FinishTraining.php:227,247` (tamamlama) · `ScheduleSessions.php:65` (oturum planı) · `DeliverTraining.php:126`

Yani **açık eğitimin yoklaması alınamıyor, günü açılamıyor, anketi okunamıyor, tamamlanamıyor.** Bu tam olarak bu hafta teslim edilen akış (tasarım `17`, duyuru `#78`).

### 10.2 · Kontenjan kontrolü firma başına uygulanıyor ◻ RAPOR

`RosterAccess.php:124-131` kontenjanı RLS'li tablo üzerinden sayıyor; `EnrolOwnParticipant.php:51` bunu kullanıyor. RLS her firmaya yalnız kendi satırlarını gösterdiği için sayım **firma başına** yapılıyor.

20 kişilik açık eğitime 5 firma 20'şer kişi yazabilir → 100 kişilik bakanlık bildirimi. Azami katılımcı mevzuatın tek sert kuralı (AD-27).

### 10.3 · Koltuk sayaçları üretimde hiç güncellenmiyor ⬛ DOĞRULANDI

`confirmed_seats`'i artıran tek yer `ReserveSeats.php:41`. Bu action'ın **production'da çağıranı yok** — grep yalnız `tests/Feature/Planlama/TrainingCapacityTest.php` ve `MinistryNotificationTest.php` döndürüyor.

`pending_seats` ve `named_seats` hiçbir yerde yazılmıyor (§11'de de geçiyor: `named_seats` hiçbir ekranda render edilmiyor).

Sonuç: `AnnounceOpenTraining.php:101,185` "kalan yer" hesabını `capacity - confirmed_seats` ile yapıyor → **her zaman tam kapasite** gösteriyor. `TrainingCapacity` teyit edilmiş satılmış koltukları hesaptan tamamen düşürüyor → satıcı dolu eğitimde boş koltuk görüp tekrar satıyor. DB CHECK kısıtı ve satır kilidi doğru yazılmış, ama tetikleyen yok.

### 10.4 · Enrolment silinince kanıt zinciri CASCADE ile gidiyor ◻ RAPOR

`training_enrolments` silindiğinde şu beşi `ON DELETE CASCADE` ile yok oluyor ve **hiçbirinde koruma trigger'ı yok**: sertifika kaydı, sınav sonucu, oturum yoklaması, katılımcı beyanı, tamamlama kaydı. `trainings` tablosu korunmuş (`2026_08_14_000200:131-143`), bu zincir unutulmuş. **Kırmızı çizgi #12.**

### 10.5 · `ScheduleSessions` imzalı yoklamayı silebiliyor ◻ RAPOR

`ScheduleSessions.php:106` oturumları silip yeniden yazıyor. Koruyucu kontrol `:63-79`'da var — ama tenant id'lerini `training_customers`'tan aldığı için **açık eğitimde hiç çalışmıyor** (§10.1). Tarih düzeltmek sertifikanın dayandığı kanıtı siliyor.

### 10.6 · Tahsilat yarış açığı ◻ RAPOR

`RecordInvoicePayment.php:78,92` bakiyeyi transaction dışında ve kilitsiz okuyor; DB tarafında da kısıt yok. 5.000₺'lik faturaya eşzamanlı iki istekle 10.000₺ tahsilat yazılabiliyor. Sınıfın kendi docblock'u bunu imkânsız ilan ediyor.

### 10.7 · Sertifika ve sınav sonucunda tekillik yok ◻ RAPOR

`RecordCertificate.php:53` ve `RecordExamResult.php:68` koşulsuz `create()` çağırıyor; enrolment başına UNIQUE kısıt yok. Bir kişiye iki sertifika veya çelişkili iki sınav sonucu yazılabiliyor — hard delete yasak olduğu için kalıcı.

### 10.8 · Audit üç action'da işlem transaction'ının dışında ◻ RAPOR

`UpdateAuthSettings.php:63/76`, `UpdateBusinessSettings.php:117/127`, `PurgeUsageLog.php:80-83/88` (sonuncusu farklı DB bağlantısında). AD-06 iki yönde kırık; `PurgeUsageLog`'da kullanım kaydı izsiz silinebiliyor — kodun kendi yorumunun yasakladığı şey. **Kırmızı çizgi #13.**

### 10.9 · Davet e-postası transaction içinde senkron gidiyor ◻ RAPOR

`InviteCustomerPerson.php:52-56`, `InvitePortalPerson.php:42-49`. `config/queue.php:44` `after_commit=false`. Geri alınan işlem sonrası müşterinin elinde çalışmayan davet kalıyor. Kardeş akış `CreateOfficeUser.php:153` doğrusunu yapıyor.

---

### 10.10 · ✅ KAPANDI — portal teklif onayı MFA istiyor (bulgu değil, kural)

Demo veri seti kurulurken çıktı (03.08.2026), **teşhis edilmedi**.

`ApproveQuote` çağrısı `AuthorizationException` veriyor. `QuotePolicy::decide` dört koşul arıyor ve dördü de sağlanıyor görünüyor:

| Koşul | Durum |
|---|---|
| `$actor instanceof CustomerUser` | ✓ |
| `sameCompany($actor, $quote)` | ✓ — `quote.customer_id` = `person.tenant_id` |
| `portalHolds($actor, 'customer.approve_quote')` | ⏸ kişide izin **var** (`getAllPermissions` doğruladı), kontrol yine de düşüyor |
| `$quote->status === 'sent'` | ✓ |

Çağrı `PermissionFamilies::using(Customer, …)` içine alındığında da sonuç değişmedi. Yani şüphe `portalHolds`'un konsol bağlamında müşteri guard'ını çözememesinde.

**Teşhis (03.08.2026):** hata değil, **kural**. `PermissionFamilies::allows()` aileyi sormadan **önce** MFA kapısını çalıştırıyor:

```php
if ($this->requiresMfaAndLacksIt($identity, $ability)) { return false; }
```

`customer.approve_quote` MFA gerektiren izinler listesinde — `financial.view`, `settings.update` ve üç `user.assign_permissions.*` ile birlikte. Kodun kendi ifadesiyle *"a code rule an administrator cannot switch off"* (spec §5.2). Demo müşteri yöneticisinin ikinci faktörü yoktu: izin vardı, onaylayamıyordu.

⚠ **Turda atlanmaması gereken adım:** portal teklif onayı **gerçekten iki faktör istiyor**. Playwright suite'i onay ekranını test ederken 2FA adımından geçmek zorunda; geçmezse "onay çalışmıyor" diye yanlış bulgu üretir.

**Sonuç:** çatı yöneticilere ikinci faktör kurulunca onay çalıştı; demo setinde onaylı teklif ve onay kaydı üretildi.

## 11 · PERFORMANS — N+1 ve sayfalama ◻ RAPOR

⚠ Bu bölüm production'a çıkışı *engellemez* ama ilk gerçek veri hacminde ekranları kullanılamaz hâle getirir. Ölçüm canlı şema ve kod okumasıyla yapıldı, profiler ile değil.

**Yapısal kök neden:** `app/Http/Controllers` altında **hiç `paginate()` yok**; `app/Queries`'teki ~140 dosyanın yalnız 15'inde herhangi bir `limit`/`take` var. Sayfalama PHP'de `array_slice` ile yapılıyor, yani veri **önce tamamen çekiliyor**.

| # | En ağır desen | Kanıt | Ölçek |
|---|---|---|---|
| N-1 | `Invoice::collected()` builder üzerinden alt sorgu kullandığı için `with('payments')` eager load'u **mimari olarak devreye giremiyor** | `Invoice.php:102-105`; çağıranlar `InvoiceController.php:68-75`; ölü eager load `:43` | 500 fatura ≈ 3.000 sorgu |
| N-2 | `FinancialOverview::receivable()` `outstanding()`'i `filter()` ve `sum()` içinde ayrı ayrı çağırıyor; `customerBalances()` üçüncü kez | `FinancialOverview.php:324,328,398` | 2.000 fatura ≈ 6.000 sorgu |
| N-3 | `companyNames()` müşteri id'si başına **ayrı transaction** açıyor (`TenantContext::execute()` dış bağlam yokken `DB::transaction()` sarıyor), üstelik 10 satıra kırpmadan **önce** | `FinancialOverview.php:452-454,424,441`; `TenantContext.php:161` | 300 müşteri ≈ 1.200 gidiş-dönüş, 10 satır basmak için |
| N-4 | `AnnouncementAudience` firma başına transaction + kişi başına Spatie yetki sorgusu | `AnnouncementAudience.php:87,294,297` | 60 firma × 50 kişi ≈ 6.000 sorgu, tek duyuru önizlemesi |
| N-5 | `InstructorCapabilityMatrix` iç içe N+1 (paket eğitimi × aday × yeterlilik) | `InstructorCapabilityMatrix.php:62` → `EligibleInstructors.php:66,70` → `InstructorCompetence.php:52,69,80` | 6 eğitim × 40 aday ≈ 2.000+ sorgu |
| N-6 | `SalesQuotes` teklif başına iki `items()` sorgusu; `with()`'te `items` yok | `SalesQuotes.php:55,84,85,154` | 800 teklif = 1.600 ek sorgu |
| N-7 | `MyQuotes` teklif başına üç sorgu (müşteri portalı) | `MyQuotes.php:61,73,186,208,220` | 200 teklif ≈ 600-800 sorgu |
| N-8 | `WhoNotifiesTheMinistry` / `WhoChecksCompletion` kişi başına gate — **ekran değil, her bildirimde koşan kuyruk yolu** | `WhoNotifiesTheMinistry.php:84,103,117`; `WhoChecksCompletion.php:44` | Yavaşlık kullanıcıya değil, bildirim gecikmesine dönüşüyor |
| N-9 | `SalesCalendar` aynı müşteriyi tek render'da **beş kez**, beş ayrı transaction'la okuyor | `SalesCalendar.php:475` ← `:131,190,289,318,448` | — |
| N-10 | `StaffCompliance` tüm join'i çekip PHP'de `array_slice` ile sayfalıyor | `StaffCompliance.php:158,168,231` | "1–4 / 238" diyen ekran 238 satırın tamamını yüklüyor |

**İndeks tarafı (§ölçüm canlı şemadan):** 96 FK kolonunun **hiçbir indeksi yok**; RLS açık 5 tabloda `tenant_id` indekssiz (`session_attendances`, `participant_declarations`, `training_completions`, `training_request_participants`, `training_suggested_participants`) — RLS politikası her sorguda o kolonu filtrelediği için **her okuma seq scan**.

**JSON filtreleme: temiz.** 18 jsonb kolonunun hiçbiri filtre yüzeyi değil.

---

## 12 · DOKÜMAN → KOD BOŞLUĞU

### 12.1 · Hiç yazılmamış akışlar (ciddiyet sırası)

| # | Akış | Doküman | Kanıt |
|---|---|---|---|
| D-1 | **Eğitim video kaydı** — Zoom kaydı → unlisted YouTube → link yalnız İdare ile paylaşılır | çatı §84-89, AD-13 | `zoom\|youtube` grep'i tüm `app/`, `database/`, `resources/js`, `config/` içinde **tek sonuç**: `DocumentPreview.vue` (yakınlaştırma). ⬛ **Sets, 03.08.2026: "gözden kaçmış."** Üstelik `ConsentText.covers_recording` bayrağı var → **var olmayan bir işlem için rıza alınıyor** |
| D-2 | **İlk eğitim / yenileme ayrımı** + yenilemede mevcut sertifika zorunluluğu | çatı §31, §142; YÖN-2019 md.8 | `training_enrolments`/`training_requests`'te kolon yok; `ONCEKI-SERTIFIKA` belge tipi seed'li (`DocumentTypeSeeder.php:133-145`) ama hiçbir action okumuyor. Tasarım `14`'ün İlk/Yenileme segmented kontrolü de uygulanmamış |
| D-3 | **İptal bildirimi** — katılımcı ve eğitmen haber almıyor | çatı §110 | `NotificationEvent.php:85` `training.cancelled` tanımlı ve kitlesi `Everybody`; `config/business.php:199-233` varsayılan kural listesinde **yok** (6 olay var), `NotificationRuleSeeder.php:39` kuralları oradan okuyor. `AssignNotifications.php:36-39` docblock'u: *"No rule means nobody is told, and that is deliberate rather than a hole."* ⬛ DOĞRULANDI |
| D-4 | **Portal enrolment KVKK rıza kapısından geçmiyor** | müşteri spec M-27 | `EnrolParticipant.php:28-35` açık rıza kapısı taşıyor; `EnrolOwnParticipant.php` içinde `consent` kelimesi **hiç geçmiyor**. ⬛ DOĞRULANDI |
| D-5 | **Sertifikada kapsanan görev alanları** | YÖN-2019 md.7/1 (*zorunlu*) | Kapsama verisi var (`training_coverage`), `certificate_records` yalnız no/tarih tutuyor (`2026_08_30_000100:267-281`) |
| D-6 | **İYS / ticari ileti onayı ve SMS** | AD-17, AD-18 | Config var (`notifications.php:28-30`), `app/Integrations/` altında yok. Bugün promosyonel gönderim olmadığı için patlamıyor; duyuru maile açılırsa 6563 ihlali |
| D-7 | ✅ **kapandı (05.08.2026) — ofis tarafı** | AD-22 revize | Ölçüm: altyapı **duruyordu** (nginx `ws.infra` bloğu, systemd unit, DNS, wildcard TLS, env anahtarları) ama uygulama katmanı **hiç yoktu**: `/broadcasting/auth` route'u mevcut değildi, `routes/channels.php` yoktu, `ShouldBroadcast` uygulayan tek sınıf yoktu, `package.json`'da `laravel-echo`/`pusher-js` yoktu. ⛔ `config/broadcasting.php` **gerekmiyordu** — framework varsayılanı `reverb` bağlantısını taşıyor; denetimin o kalemi yanlıştı.<br>⛔ **Sets kararı: yalnız ofis** *(gerekçeler AD-22'de)*. Üç guard problemi tek guard'a düştü, portal soketsiz kalarak bir risk sınıfı hiç var olmadı.<br>⚠ **Tüm bildirimler** buradan: yayın, in-app satırının kapatıldığı **tek yerde** (`Notify`) — kapsam bir alt küme değil.<br>⚠ Oturum kapanışı **dört katman** (push · auth 403 · Inertia prop · yalnız soket kopukken 60 sn nabız); zorlama zaten sunucudaydı, eklenen **ekranın haberi** |
| D-8 | **Yedek liste giriş yolu** | çatı §152 | `AddToReserveList`, `PromoteReserveEnrolment` production'da çağıransız |
| D-9 | **Eğitimin zorunlu evrakları görev üretmiyor** | çatı §168, planlama P-29 | `OpenTrainingDocumentTasks.php:31` çağıransız |
| D-10 | **Sınav tekrarı ve geçme durumu zorlanmıyor** | çatı §130-131 | `exam_retake_*` okunmuyor, `exam_results`'ta unique yok, `CompleteTraining.php:53` yalnız kaydın **varlığına** bakıyor, `passed`'a bakmıyor → başarısız katılımcı "tamamlandı" olabiliyor |
| D-11 | **Görev 4 ve 11 için CTU yetkinlik kapısı yok** | YÖN-2019 md.6/4 | `TrainingDefinitionSeeder.php:285` görev 4/11'e `IMDG-EGITICI` veriyor, CTU bağı yok |
| D-12 | **`last_taught_on` hiç yazılmıyor** — "5 yılda hiç eğitim vermeyenin belgesi yenilenmez" | çatı §20, Y-2016 md.8 | `...create_instructors.php:86` migration kendi diyor: *"Nothing writes it yet"* |
| D-13 | **3E'nin kendi yetki/belge geçerlilik takibi** | çatı §222, Y-2016 md.12 | Kurum belgesi kaydı yok |
| D-14 | **e-Fatura adaptörü + bağımsız katılımcıya fatura** | çatı §191, AD-12 | `invoices.customer_id` NOT NULL; `IssueInvoice.php:63` `Customer` şart |
| D-15 | **Katılımcı muafiyet motoru** (mühendislik diploması / denizcilik mezuniyeti) | çatı §37-38 | Kural motoru yok |
| D-16 | **IMDG müşteri ön koşulu** (gözetim yetki belgesi) | çatı §45 | grep sonuçsuz |
| D-17 | ✅ **kapandı (05.08.2026)** | AD-23 | ⛔ Reddetme doğruydu ve **cevabın tamamıydı**: yönetici "işi devret" deyip başka ekrana, kayıt kayıt elle devretmeye gönderiliyor, sonra geri dönüyordu. `HandOverWork` yazıldı — AD-23'ün üç seçeneği (**kişiye / gruba / havuza**) kapanış isteğinin kendi içinde, aynı turda.<br>⚠ **Gruba** = o grubun her aktif üyesi, çünkü erişim bu sistemde **kişi başına** ve müşteri↔grup tablosu yok; yazmak `ScopeResolver`'a çelişecek ikinci bir kaynak vermek olurdu. **Havuza** = kimseye; açık teklifin sahibi `null` olur — kapalı bir hesabın üstünde duran canlı teklif "birinin işi" gibi okunur ve kimsenin değildir.<br>⛔ Devir bir **atamadır**, dolayısıyla `user.assign_customers` **ve erişim** ister (AD-23 kural 1): hesap kapatabilmek, göremediğin bir defteri dağıtma yetkisi değildir. Boş gruba devir **reddediliyor** — havuza koymakla aynı şey değil ve denetim ikisini ayırabilmeli. Sonuçlanmış teklifler taşınmıyor (AD-11: karar verilmiş belgedeki isim, olanın kaydının parçası) |
| D-18 | **Gece vardiyası + TMGD koordinasyon uyarıları** | çatı §220, mevzuat §8 | Advisory olarak bile seed edilmemiş |
| D-19 | ✅ **kapandı (05.08.2026)** | AD-24 | AD-24 üç şey istiyordu, üçü de yoktu: **tür sürümlenir**, **her evrak hangi sürümle dolduruldu onu taşır**, ve **değişiklikten önce etki analizi gösterilir** (*"47 evrakı etkiliyor; 12'sinde expire boş kalacak"*). Türü düzenlemek düz bir kaydetmeydi — şablon altındaki evrakların altından değişiyor ve hiçbir yer bunu söylemiyordu.<br>⛔ **Değişiklik kilidi de kondu:** dolu bir alanı kaldırmak **reddediliyor** (seçenek sunulmuyor) — veri satırda ait olduğu tanım olmadan kalırdı ve silme yok (AD-11). Boş bir alanı kaldırmak serbest: red **veriye** dairdir, şekle değil.<br>⚠ Alan **eklemek** AD-24'ün (a)/(b) seçimi: *geçmişe uygula* → eski sürümle yüklenmiş evraklar `outdated` (**`missing` değil** — evrak duruyor, "yok" demek insanı yeniden yüklemeye gönderirdi), *ileriye dönük* → eski sürüm geçerli kalır. Sürüm yalnız **şekil** değişince ilerliyor; düzeltilen bir açıklama yeni sürüm değildir.<br>⚠ Sayım iki müşteri tablosunu da kapsıyor ama **`TenantContextGuard`'a altıncı istisna eklenmedi** — kapsam açıkça, her müşteri üzerinde açılıyor; aynı erişim, sınırı görünür hâlde |
| D-20 | ✅ **kapandı (05.08.2026)** — ama bulgunun tarifi değil, **tasarımın kendi cümlesi** uygulandı | AD-23 · tasarım `66` | ⛔ *"Kapsama göre sekme"* tasarım `66`'da **yok** ve olmamalı: `66:56-62` sorunu başka türlü çözüyor — *"Kuyruk yetkinize göre dolar: `approve_all` herkesi, `approve_group` yalnız kendi ağaç kapsamını gösterir"* — ve bu zaten uygulanmış. Sekme eklemek çizilmemiş bir kontrol icat etmek olurdu.<br>⛔ **Gerçekten eksik olan yarım**, tasarım `66:229-237`: *"Kimse kendi işini onaylayamaz — yetkiniz yeterli olsa da. Bu kayıt **M. Öztürk (operasyon direktörü) kuyruğunda; ona hatırlatma gönderebilirsiniz**."* İlk cümle uygulanmıştı, ikincisi **hiç** — satır "karar veremezsiniz" deyip hiçbir şey sunmuyordu: gerekçesi yazılı bir ölü uç.<br>⚠ *"İkinci imza"* AD-23 kural 2'nin ta kendisi (dört göz, otomatik bir üste çıkar) ve zaten kuruluydu; eksik olan üsttekini **isimlendirmek ve dürtmek**. `RemindApprover` yazıldı — kimin kuyruğunda olduğunu **yönetim silsilesinden** okuyor (eskalasyonun tırmandığı zincirin aynısı; "yetkiyi tutan herkes" değil, **sorumlu olan**), görev açıyor, tekrar basılınca ikinci görev açmıyor, üstünde kimse yoksa **sessizce başarılı olmuyor** — reddediyor, çünkü ağaçtaki boşluk görülmesi gereken bir şey |

### 12.2 · Yarım kalanlar — alan var, okuyan yok

`named_seats` (DB'de var, CHECK'i var, hiçbir ekranda yok) · `last_taught_on` · `record_retention_years` (form var, hiçbir sorgu okumuyor) · `exam_retake_allowed`/`exam_retake_limit` · `regulation_fallbacks` (tablo var, model ilişkisi yok) · `kums_evidence` (serbest metin, belge FK'sı değil) · yazışma triyaj kuyruğu (yazma yolu var, ekranı yok) · kontenjan teyidi kuyruğu (statü var, kuyruk ekranı yok) · `training_ministry_notifications.kind` (üç değer destekli, yalnız `initial` yazılıyor) · `analytics_usage_daily` (saatlik yazılıyor, hiçbir yerde okunmuyor) · `RenameCustomerUnit` (feature action'ı, route ve UI yok).

#### Koda karşı ölçüm *(05.08.2026)*

⚠ **On birin dördü zaten kapalı, İKİSİ yanlış teşhis edilmiş.** Denetim anlık görüntüden konuşuyordu; her kalem tek tek koda soruldu. İkisi 05.08.2026'da kapatıldı; **kapatırken iki canlı hata ortaya çıktı** (aşağıda).

#### ⛔ Ölçerken bulunan iki canlı hata *(05.08.2026)*

Organizasyon ekranının kontrollerini bağlarken çıktılar; **ikisi de benim eklediğim kod değil, zaten oradaydı** ve ikisi de aynı yeri kırıyordu: müşteri portalının yetki matrisi.

**1 · Route model binding, scope'tan ÖNCE koşuyordu.** `EnsureTenantContext` web grubuna *append* edilmiş, `SubstituteBindings` ise o grubun içinde — yani route parametresindeki her tenant kapsamlı model, isteğin scope'u daha oluşmadan çözülüyordu. `ResolvedWithinTenant` ambient scope istiyor, bulamıyor, patlıyor: **bağlı her portal adresinde 500**. Ofis tarafı ev tablolarını bağladığı için görünmüyordu. Düzeltme: `prependToPriorityList(before: SubstituteBindings, prepend: EnsureTenantContext)` — `StartSession`'dan sonra, binding'den önce; tek doğru yer.

**2 · `PermissionRows::of()` müşteri ailesinin config şeklini yanlış okuyordu.** `authorization.customer` **iç içe** (`permissions` + `default_roles`), diğer ikisi düz liste. `array_diff($names, $vocabulary)` bir dizi içeren listeyle karşılaştırılıyordu → *"Array to string conversion"*. `PermissionMatrix` bu farkı biliyordu, `PermissionRows` bilmiyordu. Düzeltme: şekil `PermissionFamily::vocabulary()`'de **tek yerde** yazılı, iki okuyucu da soruyor.

⚠ **Sonuç:** tasarım `11`'in yetki matrisi — her tik kutusu ayrı bir PATCH — **tek bir tikini bile kaydedemiyordu**. Testler yetkiyi model üzerinden verdiği ve ekranın kendi adresi hiç çalıştırılmadığı için ikisi de sessizdi. Bu, denetimin §15.1'de dürüstçe yazdığı şeyin kanıtı: *"hiçbir ekran gerçekten açılmadı"*.

| Kalem | Ölçüm | Kanıt |
|---|---|---|
| `last_taught_on` | ✅ **kapalı** | `resources/js/pages/Office/Instructors/Workload.vue` basıyor |
| `kums_evidence` | ✅ **orphan değil** — yazılıyor ve okunuyor | `CompleteTraining.php`, `Domain/Trainings/CompletionGate.php`. Bulgu "serbest metin, belge FK değil" idi: **tasarım eleştirisi**, yarım kalan değil |
| `analytics_usage_daily` | ✅ **bilinçli atıl** — üstelik "saatlik yazılıyor" **yanlış**: hiçbir şey yazmıyor | `UsageAnalytics.php:31-38` gerekçeyi yazıyor: eşik ayara indiği için rollup gereksiz kaldı, ekran `audit_usage`'ı doğrudan grupluyor. `roll_up_usage_daily()` fonksiyonu duruyor, `routes/console.php`'de **çağrısı yok** |
| kontenjan teyidi | ✅ **çözüm yolu var** | `pending_confirmation` teklif ekranında tahsis kontrolüyle çözülüyor (`Quotes/Show.vue:577,621` → `SetQuoteItemAllocation`). ⏸ Eksik olan **kuyruk ekranı**: "hangi kalemler bekliyor" sorusunu soran yer yok |
| `named_seats` | ✅ **kaldırıldı (05.08.2026)** — kolon + trigger düştü | Trigger yazıyor (`app.recount_named_seats()`, `2026_09_26`), iki sorgu prop'a taşıyor (`VisibleTrainings:135`, `TrainingDetail:53`), **hiçbir Vue basmıyor**. ⛔ Tasarım `22` de ayrı bir "adı yazılan" sayısı çizmiyor — çizdiği `4/20` `confirmed/capacity`, "+1 teyit bekliyor" ise **öneri katılımcı**, bu kolon değil. Yani ya basılacak yer bulunur ya kolon+trigger düşer: **kapsam kararı** |
| yazışma triyaj kuyruğu | ✅ **kapandı (05.08.2026)** — backoffice ekranı | `FileInboundMail.php:60,64,70` yazıyor; `resources/js` ve `routes/` içinde **tek okuyucu yok**. Tablo `resolved_at`/`resolved_by`/`filed_message_id` taşıyor — yani **bir kişinin çözmesi için** tasarlanmış. Model'in kendi docblock'u: *"a webhook that discarded it produces the worst kind of failure: one nobody can see."* Bugün tam olarak o hâlde |
| `RenameCustomerUnit` | ✅ **kapandı (05.08.2026)** — ve kapatırken iki canlı hata çıktı | Ölçünce büyüdü: organizasyon ekranı **yalnız grup oluşturabiliyor**. `moveUnit`/`closeUnit`/`setManager` route'ları var, `resources/js` içinde **hiçbiri çağrılmıyor** (`grep organizasyon/` → dört adresten biri). `RenameCustomerUnit`'in route'u bile yok. ⛔ Tasarım `11` de bu kontrolleri çizmiyor — salt okunur ağaç + "Grup Oluştur". Ama **grup yöneticisini gösteriyor** ve atayacak kontrol yok, yani gerçek portalde her grup sonsuza kadar "yönetici yok" der: **tasarım kararı** |
| `record_retention_years` | ✅ **bağlandı (05.08.2026)** | ⛔ Okuyanı **imha işi değil**: mevzuat (Y-2016 md.13/f, DG-2020 md.14/4) bir **asgari** saklama süresi koyuyor, azami değil — §10'da bu açıkça yazılı. `RecordRetention` (Domain) eğitimin **bitiş** tarihinden sayıyor, eğitim kaydı ekranı *"3 yıl — 15.03.2029'a kadar zorunlu"* diye basıyor. ⛔ Hiçbir şey silinmiyor, silinmesi planlanmıyor (Sets: *"silmeyeceğiz hiçbir şekilde, yapıyı kuracağız"*) — bir test bunu doğrudan ölçüyor |
| `exam_retake_allowed`/`limit` | ✅ **bağlandı (05.08.2026)** | ⚠ `attempt` kolonu ve `(enrolment, attempt)` unique'i **25.09.2026'da tam bu gün için** konmuştu ("düz unique yanlış şekil olurdu, geri alınması gerekirdi") — geri alınması gerekmedi. `RecordExamResult` artık tanımdan okuyor: geçilmiş sınav tekrarlanmaz, tekrar hakkı yoksa reddeder, limit **tekrar sayısıdır** (2 hak = 3 giriş). ⛔ Yan bulgu: `RecordCertificate` "herhangi bir başarısız satır" soruyordu — ilk sınavı kaybedip ikincisini kazanana **belge vermezdi**; artık son denemeye bakıyor |
| `regulation_fallbacks` | ✅ **işlevsel hâle getirildi (05.08.2026)** | ⛔ Denetim burada da yanılmış: **tablo hiç yoktu.** 29.07.2026'da yaratılmış, 31.07.2026'da kısıt setiyle birlikte düşürülmüş, `regulation_versions` 01.08'de geri gelmiş, bu gelmemişti. Yeniden yaratıldı ve zincir **uyarı çözer** hâle getirildi (`FallbackChain` + `RegulationChain` + backoffice ekranı + seeder'da TY-2019 → Y-2016, YÖN-2019) |
| `training_ministry_notifications.kind` | ✅ **üçü de yazılıyor (05.08.2026)** | ⛔ Bu "düzenli boşluk" değil **ölü uçmuş**: bildirimden sonra katılımcı eklemek `needs_kums_update`'i işaretliyor, `StartTraining` işaretliyken başlatmayı reddediyor, `NotifyMinistry` ise statü artık `planned` olmadığı için koşmuyordu. Bayrağı yalnız `RecordMinistryApproval` temizliyordu — yani **bildirim modelinde (IMDG, TY) eğitim asla başlatılamıyordu** ve hiçbir ekran sebebini söylemiyordu. `RecordMinistryUpdate` ve `RecordMinistryCancellationNotice` yazıldı, uyarı bandına çıkış eklendi, `kind`'a CHECK kondu |

### 12.3 · Doküman çelişkileri — *bunlar kod değil, doküman işi*

1. **AD-06 revize edildi, AD-21 ve çatı §192 güncellenmedi** — KamuSM RFC 3161 kaldırıldı, iki yerde hâlâ yazıyor. Kod güncel hâli uyguluyor.
2. **`mevzuat-kurallari.md` §1 ve §12 hâlâ dayatma dilinde** (*"alt sınır 70 zorunlu doğrulama"*) — AD-27'nin 27/31.07 revizyonlarıyla çelişiyor. `RegulationSeeder.php:75` metni de öyle.
3. **Program ön eki:** `planlama-spec.md` P-48 `PK` diyor, AD-19 `PR` diyor, kod `PR` uyguluyor.
4. **Açık eğitim duyurusu kanalı:** çatı §67 "bilgilendirme maili" diyor, kod bilinçli olarak yalnız uygulama içi (Sets kararı, 03.08.2026). Çatıya işlenmemiş.
5. **`omurga-spec.md` kriter 9** "2 kişiden az" sabit sayı diyor; AD-21 bunu admin ayarı yaptı.
6. ⛔ **`proje-haritasi.md` §2 "Genel durum" bayat ve kendi içinde çelişiyor:** *"Faz 3 dikey modüller: başlamadı"*, *"CI #811"*, *"sıradaki iş EGT-62"* — oysa 4.3-4.7 bölümleri o modüllerin kapandığını yazıyor. Ayrıca 4.5 *"onay kuyrukları ✗ · muhasebe ✗"*, 4.7 *"organizasyon/lokasyon ✗"*, 4.8 *"katılımcı ekran ✗ · kod yok"* diyor — **üçü de var.**

---

## 13 · BİLEŞEN VE ATOMIC DESIGN DENETİMİ ⬛ ÖLÇÜLDÜ

*(Sets'in açık sorusu üzerine ayrıca ölçüldü: ham input, componentleştirilmemiş date picker, atomic design uyumu, sidebar.)*

### 13.1 · İyi haber — ham kontrol neredeyse yok

| Ölçüm | Sonuç |
|---|---|
| `pages/` altında ham `<input>` | **2** — `Instructors/Create.vue:532` (sonek rozetli sayı alanı), `Instructors/Finance.vue:477` (checkbox) |
| Native `<input type="date\|time\|datetime-local\|month">` | **0** — yalnız `DatePicker/DateRangePicker/DateTimePicker/TimeInput` docblock'larında yasak olarak anılıyor |
| Ham `<select>` | **0** |
| Ham `<textarea>` | **0** |
| Telefon/e-posta ham input | **0** — 7 telefon ve 9 e-posta alanının hepsi `PhoneInput`/`EmailInput`; hiçbirine `:countries`/`:area-codes` taşınmıyor (arch testi koruyor) |

⛔ **Yani "componentleştirilmemiş date picker / input" endişesi karşılığını bulmadı.** Tarih ailesi tam ve zorunlu. Bu alanda gerçek sapma **kullanım hatası**: `Requests/Create.vue:163-164` `v-model:from/until` yazıp `start/end` bekleyen bileşene bağlanmıyor (K-5).

Ham HTML'e gömülü kontroller (atomu hiç yazılmamış olanlar): `Portal/Survey.vue:146,201` yıldız dizisi (`<button>★</button>`), `Quotes/Show.vue:972` eğitim seçimi (`TextInput inputmode="numeric"` — kullanıcı **veritabanı id'si yazıyor**).

### 13.2 · Katman dengesi bozuk

| Katman | Dosya | Değerlendirme |
|---|---|---|
| `atoms/` | 26 | Sağlıklı |
| `molecules/` | 33 | Sağlıklı |
| **`organisms/`** | **11** | ⚠ **Düşük.** Tekrar eden sayfa bölümleri organizma olarak çıkarılmamış |
| `templates/` | 5 | Yeterli |
| `charts/` | 8 | 6'sının çağıranı yok (§11) |

**Sonuç sayfa boyutunda görünüyor** — 12 sayfa 600+ satır, en büyük altı: `Trainings/Plan.vue` 1.359 · `Quotes/Show.vue` 1.241 · `Trainings/Show.vue` 1.068 · `Instructors/Finance.vue` 1.022 · `Instructors/Create.vue` 979 · `Agenda/Planning.vue` 901.

Katalogda çizilip organizma olarak yazılmayan, bu yüzden sayfalara gömülmüş desenler: kapasite/progress çubuğu (**9 sayfada kopya**), description list (**21 sayfada elle `<dl>`**), KPI kartı, timeline, görev satırı, collapsible satır, kalem kartı, öneri kartı, yön/tür rozetleri.

### 13.3 · `DataTable` kullanılmıyor

`organisms/DataTable.vue` var ve `02 Veri Gosterimi`'nin standardını **yarım** taşıyor (çoklu seçim + bulk bar, kayıtlı görünümler, yoğunluk seçici, kolon seçici, tfoot toplamı, numaralı sayfalama — hiçbiri yok).

Ama asıl sorun kullanım: **32 sayfa ham `<table>` yazıyor, DataTable'ı yalnız 6 sayfa kullanıyor.** Yani tablo standardının eksikleri her ekranda ayrı ayrı, farklı biçimde çözülüyor.

### 13.4 · Sidebar

**Yapı doğru:** menü tek kaynaktan üretiliyor (`OfficeNavigation.php:60-183`), `href` doğrudan `route()`'tan geliyor (`:249`), sidebar yalnız basıyor. 29 menü route adının 29'u da mevcut, çeviri anahtarları eksiksiz. *(03.08.2026'da düzeltilen ikinci adres tablosu hatası tekrarlamamış.)*

**Kalan sorunlar:**

| # | Bulgu | Kanıt |
|---|---|---|
| S-1 | `planning` grubu **7 kalem** taşıyor — HANDOFF kural 12 grup başına ≤5 diyor | `OfficeNavigation.php:79-99` (agenda, trainings, calendar, query, instructors, workload, suppliers) |
| S-2 | Tasarım `36`'nın grup bölünmesi uygulanmamış: GENEL'de olması gereken **Sorgulama** `planning` grubunda | `OfficeNavigation.php:89` |
| S-3 | **Talepler menüde yok** — route ve yetki kapısı var, kalem yok | `routes/office.php:393` ↔ `OfficeNavigation.php` |
| S-4 | `/satis/paketler` ve `/satis/antet` menüde yok | §4 G-2, G-5 |
| S-5 | Eğitmen paneli rail'i tasarım `40`'ın grup adlarını taşımıyor: `instructor_panel` "GENEL"de değil `instructor` (=EĞİTİM) grubunda | `OfficeNavigation.php:109` |
| S-6 | Rail sayacı yanlış kaleme yazılıyor: "Eğitimlerim" sayacı `instructor_calendar` anahtarına gidiyor | `RailCounts.php:84-86` |
| S-7 | **Alt menü (ikinci seviye) hiç yok** — `05 Navigasyon:44-49` çiziyor | menü tek seviye |
| S-8 | **Mobil navigasyon yok** — `AppSidebar.vue:69` ve `PortalSidebar.vue:37` `lg:flex`; 1024px altında sidebar tamamen gizleniyor, yerine hiçbir şey konmuyor. Eğitmen saha ekranları telefonda menüsüz | HANDOFF kural 12 (≤5 hedefli bottom-nav) |
| S-9 | Portal menüsünde sayaç rozetleri yok (tasarım `11:47,51` çiziyor) | `PortalSidebar.vue:64-80` |
| S-10 | Menü izni ≠ route izni — üç maddede | §2.5 |

### 13.5 · Token kullanımı

`tokens.css` handoff'un tamamını birebir taşıyor; ham hex ve arbitrary Tailwind değeri **sıfır** (arch testi `FrontendTest.php:14-40` reddediyor); fontlar repo içinden, CDN çağrısı yok.

Kırık olan tarafı §3 K-1: **tanımsız token adına yazılmış utility'ler** — arch testi bunu görmüyor.

Ayrıca `resources/css/app.css:6-9` hâlâ Laravel varsayılanı `--font-sans: 'Instrument Sans'` tanımlıyor; token seti `Archivo` diyor — ikinci ve çelişen bir tanım.

---

## 14 · YOL HARİTASI

⚠ Bu bir **sıralama önerisidir**, onaylanmış plan değil. Sets ile birlikte kararlaştırılacak.

### Kuşak 0 — production'a çıkışı engelleyenler (altyapı) ✅ **TAMAMI KAPANDI**

⛔ **`.env` sürüklenmesi kalıcı olarak kapatıldı** (`tests/Arch/EnvExampleTest.php`, üç kural):

1. **Şablonun tanıdığı bir servis ailesindeki her anahtar aynı yazılmalı.** Kural aileye göre — dosyaya ya da varsayılana göre değil. Dosyaya göre olsaydı `config/services.php` muaf tutulurdu ve hata tam oradaydı; varsayılana göre olsaydı `SESSION_CONNECTION` gibi "boş olması zaten anlamlı" anahtarlar yakalanırdı. Şablon bir kez `POSTMARK_` ailesini tanıdıysa, o ailenin okunan her anahtarı orada yazmalı.
2. **Şablondaki hiçbir anahtar okunmadan duramaz.** Doldurulup hiçbir şeyi yapılandırmayan bir alan, eksik olandan kötüdür. 22 anahtar bu yüzden `⏸ HENÜZ BAĞLANMAMIŞ` başlığı altına yorumlandı (SMS, İYS, KamuSM, Sentry, Discord).
3. **İki şablon aynı anahtar kümesinde kalmalı.** Unutulan hep production'dır — çünkü kimse deploy gününe kadar açmaz.

⚠ Kural regresyonla kanıtlandı: `POSTMARK_API_KEY` eski ismine döndürüldüğünde **iki yön de** kırıldı.

Ayrıca kapatılanlar: **P-7** `session.secure` artık local dışında varsayılan `true` (önceden `null` → `false`) · **P-8** production şablonunda `DB_SSLMODE=require` · **P-9** `public/fixtures` zaten `.gitignore`'da ve deploy `git clone` ile çalıştığı için üretilmiyor.

| # | İş | Bölüm |
|---|---|---|
| 0.1 | ✅ **KAPANDI 03.08.2026** — `docker-compose.{postgresql,pgbouncer}.prod.yml` (`ports: !override` + `127.0.0.1:`), `pgbouncer.prod.ini` (`listen_addr` loopback, `auth_type` scram) | §1.1 |
| 0.2 | ✅ **KAPANDI 03.08.2026** — `deployment/nginx/akademi.3egrp.com.conf`: beş host, HSTS/CSP başlıkları, giriş uçlarına ayrı hız sınırı bölgesi, `nginx -t` ile doğrulandı | §1.3, §9 |
| 0.3 | ✅ **KAPANDI 03.08.2026** — `panel.` host'u, `routes/panel.php`, `/portal` öneki silindi, landing page yazıldı | §1.4, §1.5 |
| 0.4 | ✅ **KAPANDI 03.08.2026** — `GET /giris`, magic link halleri, kimlik teyidi, ikinci faktör, şifre sıfırlama 1/2 ve 2/2. Kapsama matrisi: `docs/reviews/tasarim-10-musteri-kapsama.md` (4 ⚠ soru açık) | §1.5, §5.1 |
| 0.5 | ✅ **KAPANDI 03.08.2026** — `deploy.sh` (release dizini + atomik symlink + bakım modu + `trap` ile her hâlde bakımdan çıkış), `backup.sh` (deploy öncesi zorunlu + `--verify` ile geri yüklenebilirlik denemesi) | §1.2 |
| 0.6 | ✅ **KAPANDI 03.08.2026** — `deployment/systemd/`: horizon · reverb · schedule (timer, dakikada bir) · backup (timer, 03:15) | §9 |
| 0.7 | ✅ **KAPANDI 03.08.2026** — `.env.production.example` üretildi; Postmark sürüklenmesi düzeltildi; **üç arch testi** bu sınıfı kapattı (aşağıda) | §9 P-4, P-5 |
| 0.8 | ✅ **KAPANDI 03.08.2026** — `HorizonServiceProvider`: `settings.view` tutan backoffice kimliği. Dört testle pinlendi | §1.1b |

### Kuşak 1 — veri bütünlüğü ve yetki (çıkıştan önce) ✅ **TAMAMI KAPANDI**

| # | İş | Durum |
|---|---|---|
| 1.1 | Açık eğitimde `training_customers` bağlanması | ✅ Her iki kayıt yolu da firmayı bağlıyor (`EnrolOwnParticipant`, `EnrolParticipant`) |
| 1.2 | Kontenjan kontrolünü tenant kapsamından çıkar | ✅ `app.training_seats_taken()` (SECURITY DEFINER, tek tam sayı döner) + `SeatCount`; iki regresyon testi |
| 1.3 | `ReserveSeats`'i teklif onayına bağla | ✅ Onayda **kalem kalem** rezervasyon; yer yoksa kalem `pending_confirmation`'da bekler; `pending_seats`/`named_seats` trigger ile türetiliyor |
| 1.4 | Filament üç kaynağa policy | ✅ `GroupPolicy`, `CustomerSegmentPolicy`, `SurveyQuestionPolicy` + 12 izin + arch testi + 9 feature testi |
| 1.5 | Kullanım logu route desenleri + filtre maskeleme | ✅ `office.*` / `customer.*` / `filament.backoffice.*`; Filament paneline `LogUsage`; `Masker::searchTerm()` şekle göre maskeliyor; **gerçek istekle** dört test |
| 1.6 | Magic link kimlik teyidine throttle + kilit + audit | ✅ `ThrottlesSignIn` paylaşıldı; başarısızlık ve başarı audit'leniyor |
| 1.7 | `/yonetim`, `/onaylar`, `outreach` yetki kapıları | ✅ `OfficeNavigation::APPROVAL_RIGHTS` tek liste; kartlar kendi izniyle süzülüyor (`financial.view`, `instructor.view`) |
| 1.8 | Enrolment CASCADE zincirine koruma trigger'ları | ✅ `training_enrolments` + `exam_results` + `certificate_records` + `training_completions` deny trigger'ları |
| 1.9 | Tahsilat yarışı + sertifika/sınav tekilliği | ✅ Bakiye kilit altında yeniden okunuyor; `certificate_records` enrolment başına UNIQUE; `exam_results` (enrolment, attempt) UNIQUE |
| 1.10 | Audit'i transaction içine al | ✅ Üç action; `PurgeUsageLog`'da sıra izsiz silmeyi imkânsız kılacak şekilde seçildi |
| 1.11 | Dosya yüklemede sunucu tarafı tip/boyut doğrulaması | ✅ `AcceptableUpload` — üç yükleme action'ı; `document_types` alanları artık sunucuda okunuyor; çalıştırılabilir uzantılar her hâlde reddediliyor |
| 1.12 | Trusted proxy | ✅ `security.trusted_proxies` + `AppServiceProvider`; nginx forwarded başlıklarını **üzerine yazıyor** (append değil) |
| 1.13 | Müşteri girişine hız sınırlaması | ✅ Giriş ve müşteri ikinci faktörü aynı sayaç sınıfında |
| 1.14 | Portal okuma ekranlarında izin ayrımı | ✅ `PortalScreen::portalUserHolding()`; beş ekran kendi iznini istiyor; 10 test |

⛔ **Yan bulgu — 21 kırık test.** `omurga-rls` grubunda 21 test panel host taşımasından beri kırıktı ve fark edilmemişti: müşteri giriş/şifre/magic link akışları JSON döndürmeyi bırakıp Inertia ekranına döndü, assertion'lar 200 beklemeye devam etti. Hepsi düzeltildi. Ayrıca `local` diskinde `visibility` **yazılı değildi** — `SensitiveDataTest` tam da bunu okuyor ve testin işaret ettiği garanti, evrakların koşturulduğu diskte doğrulanmamış duruyordu.

⛔ **Yan bulgu — hız sınırı sayacı testler arasında sıfırlanmıyordu.** `CACHE_STORE=array` tüm süreç için tek nesne; müşteri kapısına limit gelir gelmez yirmi test, konusuyla ilgisi olmayan bir 302 ile düştü. Saat gibi paylaşılan durum: artık her testten önce temizleniyor.

⛔ **1.3'ün kararı — gün bazlı kalem koltuk tutmaz** *(Sets, 04.08.2026)*. Soru şuydu: gün bazlı fiyatlanan bir kalem kaç koltuk kesinleştirir? Cevap: **hiç.** *"Gün bazlı verilen teklifte kişi sayısı bizi sadece iki konuda ilgilendirir: bakanlığın asgari ve azami limiti. Bunun dışındaki kısım müşteriye kalmış."* Satılan şey bir günlük eğitimdir; odaya kaç kişi oturacağı firmanın kendi kararıdır ve onu yalnız mevzuat sınırlar — faturalanan tutar değil.

Bunun iki sonucu oldu:

1. **`named_seats <= confirmed_seats` CHECK kısıtı DÜŞTÜ.** Var olmayan bir kuralı kodluyordu. Hiç tetiklenmemişti çünkü `named_seats` hiç yazılmıyordu; yazılmaya başladığı anda, parasını tam ödemiş bir firmanın **ilk personelini** listeye eklerken patlayacaktı. Kalan sınır `confirmed_seats <= capacity` — ve `capacity` mevzuatın azamisini taşıyor, yani duran taraf ticari değil mevzuata dayalı olan.
2. **`TrainingCapacity` aynı koltukları iki kez sayıyormuş.** `confirmed + pending` toplamını "dolu" sayıp, aynı teklif kalemlerini bir kez daha "teklif bekleyen" diye ekliyordu. Sayaç sıfırda kaldığı sürece görünmüyordu; yazılır yazılmaz 20 kişilik eğitim 9 boş yeri **sıfır** gösterdi. Canlı toplam düzeltilmedi, **kaldırıldı** — çünkü ikinci bir hatası daha vardı: `quantity`'yi her kalem için topluyordu ve gün bazlı kalemde o alan **gün** demek. "İki gün" buraya "iki koltuk" olarak geliyordu.

⚠ Teklif metnindeki *"bu eğitimde N boş yer var"* cümlesi de düzeltildi: kalemin **kendi** talebi artık boş yerden düşülmüyor. Dokuz kişi isteyen birine, o dokuzu kendisi doldurduğu için "hiç yer yok" demek, reddetme gibi okunan bir aritmetikti.

### Kuşak 2 — sessiz kırıklar ve erişilemeyen ekranlar ✅ **TAMAMI KAPANDI**

✅ **K-1…K-6 KAPANDI (03.08.2026).** Tanımsız token'lar (274 kullanım, 66 dosya) + arch testi · `Overview.vue`'nun dört grafik import'u · `/egitimler/planla` → `/egitimler/yeni` · `Plan.vue`'nun `form` → `plan` düzeltmesi · `DateRangePicker`'ın `from/until` → `start/end` bağlanması + arch testi.

✅ **G-3 KAPANDI** — portal eğitim detayına tasarım `15`'in chevron kolonuyla girilebiliyor.

✅ **KALAN GİRİŞ NOKTALARININ TAMAMI KAPANDI (04.08.2026).**

| # | Ne yapıldı |
|---|---|
| G-1 | Teklif çıktısına iki giriş: satışçı için `Quotes/Show`'da "Teklifi yazdır", **müşteri için** `panel.<domain>/teklifler/{q}/yazdir` — imzalanacak nüsha artık var. ⛔ Müşteri çıktısı **mühürlenmiş gövdeyi** basar (hash'lenen metnin ta kendisi), lines'ı yeniden dizen ikinci bir şablon değil: öyle olsaydı imzalanan kâğıtla delil birbiriyle çelişebilirdi |
| G-2 | Paket kataloğu satış menüsünde (`ContentPackage` policy'si üzerinden) |
| G-4 | Planlamacının talep kutusu planlama menüsünde |
| G-5 | ⚠ **Bulgu değildi.** `/satis/antet` bir ekran değil, antet baytlarını akıtan uç — ve `Printout.vue` onu zaten `<img src>` ile kullanıyor. "Menüde yok" doğru ama menüde olmaması gerekiyor |
| G-6 | Gerçekleşen maliyet girişi eğitim detayındaki maliyet satırlarında. Sapma gerekçesi **istenir, dayatılmaz** — zorunlu olup olmadığına planlananla karşılaştırarak action karar verir |
| G-7 | "Fatura kes" onaylı teklifin kendi ekranında. ⚠ Kalem gönderilmez: onaylı teklif **bütün** olarak faturalanır (M-18) ve action kalemleri teklifin kendisinden üretir — yeniden yazan bir form, müşterinin onayladığından sapabilecek ikinci bir sürüm olurdu |
| G-8 | Reddedilmiş talebe "Yeniden aç" |
| G-9 | Eğitmen kendi gün ekranından **boş yoklama listesini** basabiliyor. ⛔ Uç `view` ile çözüldüğü için eğitmen 404 alıyordu: eğitmen hiçbir planlama izni tutmaz, ve imzalı hâlini yükleme alanı zaten o ekranda duruyordu. Yeni `printRegister` yetkisi "eğitimi görebilen **ya da** onu veren" diye sorar; geri kalan herkese hâlâ 404 (varlığını ifşa etmemek için) |

⛔ **Bu sınıf için kalıcı test yazıldı** (`ReachableFeatureTest`): her kalem için `resources/js` içinde onu adlandıran bir şey var mı. ⚠ Tarayıcı turu bunu göremez — tur ekranları **adresle** ziyaret eder, dolayısıyla erişilemez bir sayfa ona sapasağlam görünür. Eksik olan ekran değil, kapıdır.

⛔ **Token kuralı yeniden yazıldı ve üç sessiz hata daha çıktı.** Eski kural, ilk parçası token ailelerinden biri olmayan sınıfları atlıyordu — yani **var olmayan bir aileyi adlandıran sınıf, tam da var olmadığı için** denetim dışı kalıyordu. `border-line` `SlideOver`'da yazıldığı günden beri duruyordu; `text-danger` görev ekranındaki **dosya yükleme hata mesajının rengiydi** ve paragrafın kendi rengiyle çiziliyordu; `text-label` dört yerde hiçbir şey yapmıyordu. Kural artık aile beyaz listesi değil, **izin listesi**: Tailwind'in kendi anahtar kelimeleri, tipografi token'ları ve avuç içi kadar yön/kenar sözcüğü dışında her şey şüphelidir. Regresyonla doğrulandı.

✅ **Kalan ikisi de kapandı:** `Instructors/Show.vue` `SelectInput`'u import etmiyordu (K-2'nin aynısı, beşinci vaka) · `auth.second_factor.invalid` sözlükte yoktu — ikinci faktör yanlış girildiğinde kullanıcı ham anahtar metnini görüyordu.

⛔ **İki arch testi bu sınıfı kapattı:** *"imports every component it renders"* (render edilen her bileşen import edilmiş mi) ve *"binds only props the control actually declares"* (bileşenin tanımlamadığı `v-model:` adına bağlanmak). İkisi de derleme zamanında kırıyor; tarayıcı turu aynı soruları render edilmiş sayfaya soruyor. Biri diğerinin yerine geçmiyor: statik tarama çalışma anında kurulan adı göremez, tur da açılmayan bir dalı.

### Kuşak 3 — iş kuralı boşlukları ✅ **TAMAMI KAPANDI (04.08.2026)**

⛔ **Kararlar Sets'e soruldu ve alındı** — üçü de kodun şeklini değiştirdi:

1. **Video kaydı OTURUM başına.** Zoom bir toplantıyı kaydeder, iki günlük eğitim iki toplantıdır. Eğitim başına tek kolon, birine hangi günün bağlantısını saklayacağını seçtirirdi. *"Bakanlık tek dosya isterse bakarız, ama oturum başı yapalım"* — iki bağlantıyı birleştirmek yapılabilir bir iştir, birini bölmek değildir.
2. **Yenilemede önceki sertifika UYARIR, engellemez.** AD-27'nin çizgisi: mevzuat kuralı okunan bir cümledir, yazılımın kapattığı bir kapı değil. Kâğıt bir klasörde olabilir ya da yarın gelebilir; reddeden bir sistem, etrafından dolaşılan bir sistemdir.
3. **`sinav_zorunlu` yanına `video_kaydi_zorunlu`** *(Sets'in önerisi)*. *"İleride video gerektirmeyen eğitim ya da sınavsız iç mesleki eğitim olursa bunlar sistemi kitlemez."* Kırmızı çizgi 14'ün ta kendisi: koda gömülen kural düzeltilemeyen kuraldır.

| # | Ne yapıldı |
|---|---|
| D-1 | `training_sessions.recording_url` + `recorded_at` + `recorded_by`; `RecordSittingRecording`; **kendi izni** (`training.recording.view` / `.record`). ⛔ Hiçbir panelde görünmez — müşteride de, katılımcıda da, **eğitmende de**. Bunu bir aramayla ölçen test var: kural "hiçbir ekran çizmiyor", ve tek ekran testi ancak o ekranın çizmediğini kanıtlar. ⚠ `covers_recording` yıllardır rıza metnindeydi: **var olmayan bir işlem için rıza alınıyordu** |
| D-2 | `training_request_definitions.kind` + `trainings.kind` (`first`/`renewal`); `RenewalEvidence`; yoklama listesinde madde referanslı uyarı. ⚠ `ONCEKI-SERTIFIKA` belge türü, açıklamasında bu kuralı yazılı taşıyarak katalogla birlikte doğmuş ve **bugüne kadar hiçbir kod onu okumamış** |
| D-3 | `training.cancelled` varsayılan bildirim kurallarına eklendi — katılımcı, eğitmen, müşteri sorumlusu. ⚠ **Bant yok**: iptal bir olaydır, olduğu anda bir kez söylenir; bant, "eğitim hâlâ olmuyor" hatırlatması olurdu |
| D-4 | `EnrolOwnParticipant` artık KVKK rıza kapısını taşıyor. ⛔ Ofis yolu bu kapıyı ilk günden taşıyordu; **portal yolu taşımıyordu** — yani firmanın kendi koordinatörü, listenin tek kapısızı yoluydu. Oda artık filme alındığı için daha da ağır |
| D-5 | `certificate_records.scope_name` + `covered_scopes`. ⛔ **Dondurulmuş, join değil**: kapsama bir katalogdur ve katalog düzenlenir; görev 8'in neyi kapsadığı düzeltildiği gün, verilmiş her sertifika sessizce başka bir şey iddia etmeye başlardı |
| D-8 | Yedek listeye ekleme ve **çağırma** giriş noktaları. ⚠ Liste boşken de çiziliyor: düğmesiz boş liste, olmayan bir özellikten ayırt edilemez |
| D-9 | `training_documents` tablosu + `UploadTrainingDocument` + görevlerin **teslimde** açılması. ⛔ `OpenTrainingDocumentTasks` yazıldığından beri evrak istiyordu ve **yüklenecek bir yer yoktu** — çağıranı da olmadığı için kimse teslim edemediği bir evrakla karşılaşmamıştı. ⚠ İmzalı yoklama listede yok: günlük akışta zaten zorunlu |
| D-10 | `CompleteTraining` sınav evrakını da arıyor; **`RecordCertificate` artık `passed` okuyor** — sınavdan kalana sertifika kaydı girilebiliyordu ve hard delete yasak olduğu için kalıcı olurdu |
| D-11 | Görev 4 ve 11 tanımlarına `CTU-EGITICI` şartı (md.6/4). ⛔ Yetkinlik kapısı **çalışıyordu** — eksik olan şartın kendisiydi. *(Sets: "eğitmen atandığı eğitimin gerekliliğini taşımak zorunda")* ⚠ md.6/5'in izin verdiği "iki eğitmen birlikte karşılar" hâli sunulmuyor: yetkinliği iki kişiye yayılmış bir atama, bakışta denetlenemez |
| D-12 | `last_taught_on` teslimde yazılıyor, yalnız ileri doğru. ⛔ Migration'ın kendi yorumu *"nothing writes it yet"* diyordu ve hiçbir zaman yazan olmadı; hep-null bir kolon *"kimse hiç eğitim vermemiş"* diye cevap veriyordu |

⛔ **`EGITIM-DOKUMANI` belge türü emekliye ayrıldı.** İki mekanizma aynı şeyi anlatıyordu ve birbirinden habersizdi: `TrainingMaterial` (tanıma bağlı, backoffice'ten yönetilen, eğitmenin sahada gördüğü — çalışıyor) ve bu belge türü (hiç yazılmamış, hiç okunmamış). ⚠ İkisinin gerçekte çizdiği ayrım **öncesi ve sonrası**: boş yoklama şablonu ve sınav soru kâğıdı *öğretilen* şeydir; imzalı yoklama ve sonuç tutanağı *sonradan dosyalanan* kanıttır. Sunumun "sonrası" yoktur — bu yüzden o satırın yapacak işi yoktu. Silinmedi, pasifleştirildi (AD-11).

### Kuşak 4 — tasarım tamamlama

⛔ **Sıra kararı Sets'in** *(04.08.2026)*: *"önce katalog boşluklarını
tamamlayalım ki sonra yirmi ekranda bu katalog componentlerine sadık düzeltme
yapabilelim."* Doğru sıra — ekranlar bu atomları kullanacak; tersi olsaydı yirmi
ekran iki kez elden geçerdi.

✅ **§5.2 KATALOG BOŞLUKLARI KAPANDI (04.08.2026)** — kapsama matrisi:
`docs/reviews/katalog-kapsama-2026-08-04.md` (dosyalar yeniden açılarak
üretildi, §5.3'ün dersi gereği).

On iki atom yazıldı: rating yıldızı · limit işaretli slider · progress çubuğu ·
description list · KPI kartı · timeline · tooltip · popconfirm · skeleton (atom +
tablo) · mevzuat callout · tab şeridi · bildirim merkezi. Ayrıca Checkbox'ın
**indeterminate** ve hata durumu, Toast'ın üçüncü tonu ve ✕'i.

⛔ **En ağır bulgu mobil navigasyondu.** `lg` altında **her iki rail de** gizliydi
ve yerine hiçbir şey konmamıştı: 375px'te eğitmen panelinde **gezinme yoktu** —
sahada telefonla duran kişi, gönderildiği ekrana ulaşıp başka hiçbir yere
gidemiyordu. `BottomNav` hedeflerini **rail'in kendi listesinden** alıyor; elle
tutulan ikinci bir liste, rail'in kurulduğu izinlerden sapar ve sapma 403 veren
bir bar hedefi olarak görünürdü.

⛔ **A4 baskı kuralları yazıldı** ve baskı renkleri **token katmanına** kondu.
Ekran paleti değiller: mürekkep saf siyah, çünkü `ink-950` mono lazerde gri
basar; kâğıt saf beyaz, çünkü tonlu zemin toner yer ve üstündeki yazıyı yutar.

⚠ **Üç varyant bilinçli olarak ertelendi** (TextInput loading, OTP geri sayımı,
FileDrop yüzdesi): üçü de bir DAVRANIŞA bağlı, bir görünüme değil — arkasında
hiçbir şey olmayan bir gösterge, video kaydı olmadan alınan rıza gibi olurdu.
İhtiyaç duyan ekranla birlikte yazılacaklar.

⛔ **Turun aynı turda bulduğu iki taşma:** telefon dolgusu 28px'ten 16px'e
inince, o güne kadar dolgunun gizlediği iki sabit genişlik açığa çıktı —
`DateRangePicker`'ın 320px sabit açılır kutusu ve bir sayfanın `-mx-7` sabit
taşması. İkisi de düzeltildi. ⚠ Bunlar yeni hata değil; **görünür hâle gelen eski
hata**: mutlak konumlu bir kutu taşsa bile sayfayı genişletir.

⏸ **Kalan:** §5.1'deki 20 yazılmamış ekran · §13.2 organizma çıkarma · §13.3
DataTable'ın `02` standardına tamamlanması ve 32 sayfanın taşınması · §13.4
sidebar düzeltmeleri · 21 sayfanın `DescriptionList`'e ve 9 sayfanın
`ProgressBar`'a taşınması. ⚠ Hepsi ekran turuyla birlikte: hangi ekranın
`DataTable`'ın hangi yeteneğine ihtiyacı olduğu, ekran açılmadan bilinemez ve
altısını birden yazmak kullanılmayan beş özellik üretir.

### Kuşak 5 — performans

✅ **İndeksler kapandı** *(05.08.2026)*: indekssiz yabancı anahtar **137 → 0** (denetim 96 saymıştı — tablolar geldikçe büyümüş), RLS `tenant_id` indeksi **0/5 → 9/9** (denetim 5 saymıştı, katalog 9 dedi). İki test canlı şemayı ölçüyor: `tests/Feature/Omurga/ForeignKeyIndexTest.php`. ⚠ Migration RLS tablolarını **elle listelemiyor, kataloğa soruyor** — elle liste tam olarak indekslerin bayatladığı gibi bayatlar.

✅ **N+1'ler kapandı** *(05.08.2026)*: N-1…N-9 iki commit'te, **N-10 kendi turunda**. `StaffCompliance` artık sayfayı, toplamları, firma katlamasını, en ağır ihtiyaçları ve erişim logunun id'lerini **aynı SQL kümesine sorulan altı soru** olarak okuyor — sonuç kaç satır olursa olsun sorgu sayısı sabit (`StaffQueryTest`: küçük ve büyük firma **eşit** sorgu sayısı). Kimlik numarası tenant anahtarıyla şifreli olduğu için **yalnız sayfadaki 25 satır** çözülüyor; eskiden tüm sonuç çözülüp dokuzda biri basılıyordu.

⚠ Bilinçli üç davranış değişikliği docblock'ta yazılı: sıralama artık Postgres kolasyonu (ürünün geri kalanıyla aynı), işe giriş vadesi ay sonuna **kırpılıyor** (31 Ocak + 3 ay = 30 Nisan), ve her sıralama `person_id` ile bitiyor — tiebreak'siz sayfalama aynı kişiyi iki sayfada gösterir.

⏸ **Kalan (öneri, karar Sets'in):** `InstructorWorkload` hâlâ PHP'de sayfalıyor. Ama kümesi **3E'nin kendi eğitmen kadrosu** — onlarca satır, binlerce değil — ve iki kolonu dönem penceresini bilerek yok sayıyor, yani SQL'e indirmek doğruluk yüzeyi olan bir yeniden yazım. Ölçülen fayda küçük görünüyor; **dokunmadım.**

### Kuşak 6 — doküman bakımı

§12.3'teki 6 çelişki · `proje-haritasi.md` §2'nin güncellenmesi · kapsama matrislerinin bağımsız doğrulamayla yeniden üretilmesi (§5.3).

---

## 15 · BU DENETİMİN KAPSAMADIKLARI

Dürüstlük gereği: aşağıdakiler **bakılmadı**, dolayısıyla "temiz" denemez.

1. **Tarayıcıda çalıştırma.** Hiçbir ekran gerçekten açılmadı; K-1…K-5 sınıfı hatalar kod okumasıyla bulundu. Aynı sınıftan **başkaları olabilir** — tek güvenilir yol Playwright ile her ekranı açıp konsolu okumak.
2. **Gerçek veriyle performans.** N+1 bulguları kod okumasından; profiler koşulmadı.
3. **Erişilebilirlik.** Klavye gezinmesi, odak sırası, ARIA, kontrast oranları hiç ölçülmedi.
4. **Yük ve eşzamanlılık testi.** Yarış açıkları (§10.6) kod okumasıyla bulundu, tetiklenerek doğrulanmadı.
5. **Filament backoffice ekranlarının tasarım sadakati.** 16 resource'un form/tablo kompozisyonu tasarım dosyalarına karşı denetlenmedi (tasarım `61`-`68` ofis tarafını kapsıyor).
6. **E-posta/PDF çıktılarının görsel doğrulaması.** Şablonlar okundu, render edilmedi.
7. **Mevzuat metinlerinin kendisi.** `mevzuat/` arşivindeki resmî metinler bu turda açılmadı; mevzuat bulguları `docs/mevzuat-kurallari.md` üzerinden yürüdü.

