# Canlı Ders Entegrasyonu — Durum ve Test Rehberi

Bu dosya, `docs/CANLI-DERS-VIDEO-CHART-SENKRON-RAPORU.md`'daki plana göre şu ana kadar
**gerçekten kodlanmış ve test edilmiş** olanı özetler. Rapor "ne yapılmalı"yı anlatır,
bu dosya "ne yapıldı ve nasıl test edilir"i anlatır.

Tarih: 2026-08-04

---

## 1. Genel durum

| Faz | Açıklama | Durum |
|---|---|---|
| Faz 1 | Altyapı (Reverb, `live_sessions` tablosu, kanal yetkilendirmesi) | ✅ Tamamlandı |
| Faz 2 | Chart senkron (`setActive`/`ajaxRefreshTransit` wrap) | ✅ Tamamlandı |
| Faz 2b | Tam state senkronu (`request_sync`/`full_sync`) | ✅ Tamamlandı |
| Faz 3 | Video (WebRTC P2P sinyalizasyonu) | ✅ Tamamlandı |
| TURN | Ücretsiz TURN havuzu entegrasyonu (kod hazır) | ⏳ Sadece API key bekleniyor |
| Faz 4 | Öğretmen paneli (gerçek `panel.blade.php`'ye bağlama) | ✅ Tamamlandı |
| Faz 5 | Whiteboard (çizim) | ✅ Tamamlandı |
| Faz 6 | Ders kayıtları | ✅ Tamamlandı |
| Faz 7 | Cilalama (reconnect, mobil, saklama politikası) | ✅ Reconnect doğrulandı — mobil/saklama açık karar bekliyor (bkz. §9) |
| "Canlı ders başlat" UI | Host toolbar butonu + Ders Kayıtlarım linki | ✅ Tamamlandı |
| **Rework (2. tur)** | Kamera → sol üst sub-chart slotu, modern UI, kritik bug fix'leri | ✅ Tamamlandı (bkz. §10) |
| Sunucu kurulumu | NSSM, ARR, coturn (Windows VDS) | ⏸️ Bilinçli olarak en sona bırakıldı |

---

## 2. Mimari özet

- **Sinyal/senkron altyapısı:** Laravel Reverb (self-hosted WebSocket, Pusher protokolü uyumlu).
- **Roller:** Öğretmen = `users.role === 'astrologer'` (mevcut ama kullanılmayan bir alan, yeniden
  kullanıldı — yeni migration gerekmedi). `User::isTeacher()` / `isStudent()`.
- **Oturum modeli:** `live_sessions` tablosu (`teacher_id`, `student_id` nullable, `room_id` uuid,
  `status`, `started_at`, `ended_at`). Öğretmen oturum açar, öğrenci linke tıklayınca
  "claim" eder (`LiveSessionController::join`).
- **Kanal:** `private-session.{room_id}` — sadece o oturumun öğretmeni/öğrencisi girebilir
  (`routes/channels.php`).
- **Chart senkronu — yazma yetkisi asimetrik:** Sadece öğretmen (host) chart event'i
  yayınlayabilir (`LiveSessionController::publish`, backend'de zorunlu kılınır). Tek istisna:
  öğrenci `request_sync` gönderebilir ("bana mevcut tüm haritaları gönder" sinyali, chart verisi
  taşımaz).
- **WebRTC sinyalizasyonu — kasıtlı olarak simetrik:** offer/answer/ICE candeyi her iki taraf da
  gönderebilir (`LiveSessionController::signal`), çünkü görüşme kurulumu doğası gereği iki yönlü.
- **Video/ses verisi sunucudan hiç geçmiyor** — sadece bağlantıyı kurmak için gereken SDP/ICE meta
  verisi Reverb üzerinden taşınıyor (ham P2P WebRTC).
- **Tek event sınıfı:** `App\Events\LiveSessionEvent` — `type` alanına göre (`setActive`,
  `ajaxRefreshTransit`, `full_sync`, `request_sync`, `webrtc.offer`, `webrtc.answer`,
  `webrtc.ice-candidate`, `webrtc.hangup`, `test.ping`) farklı davranır. Yeni bir senkron türü
  eklemek için yeni bir class yazmaya gerek yok, sadece yeni bir `type` string'i yeterli.
- **Faz 4 — gerçek panele bağlanma mantığı:** `/panel/live/{roomId}` artık gerçek
  `panel.blade.php`'yi render ediyor (`IndexController::index()` üzerinden, `$liveSession`
  adında opsiyonel/additive bir parametreyle). **"Kimin haritası" sorusu ayrıca çözülmedi** —
  öğretmen kendi hesabıyla, öğrenci kendi hesabıyla girer (panelin normal davranışı,
  hiç değişmedi); ekrana gerçekte ne görüneceğini `request_sync`/`full_sync` akışı belirliyor
  (öğretmenin ekranındaki tüm açık haritalar öğrenciye anında kopyalanır). Öğrenci girişinde
  kısa bir an kendi (normal) chart'ı görünüp hemen öğretmeninkiyle değişebilir — bu bilinen,
  kabul edilmiş bir MVP davranışı.
- **Öğrenci input kilidi:** `<body class="... live-session-readonly">` (sadece öğrenci
  rolünde) + CSS `pointer-events:none`. `.live-session-controls` class'ı bu kilidin dışında
  tutuluyor (video/host toolbar overlay'i, `partials/live-session-controls.blade.php`).
- **`panel.js`'e hâlâ hiçbir satır eklenmedi/değiştirilmedi** — bkz. aşağıdaki not.
- **Faz 5 — whiteboard:** `#mainDropzone`'un üstüne (chart DOM'una dokunmadan) şeffaf bir
  `<canvas>` overlay ekleniyor (`live-whiteboard.js`). "Dondurma" = canvas'ı görünür +
  (sadece host için) tıklanabilir yapmak. Stroke'lar 0-1 aralığında normalize koordinatlarla
  (`{x,y}`) yayınlanıyor — öğretmen/öğrenci ekran boyutu farklı olsa bile doğru orantı korunur.
  Sadece host çizer/yayınlar; `full_sync` artık mevcut stroke'ları da taşıyor (öğrenci geç
  katılırsa çizimleri de görür). Kaydetme: `session_annotations` tablosu (vektör stroke +
  opsiyonel PNG export).
- **Faz 6 — kayıt (event-log tabanlı, rapor Bölüm 11.2 Yöntem B):** Ekran video olarak
  KAYDEDİLMEZ. Sadece öğretmenin kamera+ses akışı (`live-webrtc.js`'in zaten açık olan
  stream'i, `getLocalStream()` ile paylaşılıyor) `MediaRecorder` ile kaydedilip seans sonunda
  tek seferde yükleniyor. Chart/çizim değişiklikleri ayrı, timestamp'li bir tabloda
  (`live_session_events`) otomatik loglanıyor — `LiveSessionController::publish()` içinde,
  `recording_enabled` true olduğu sürece her `setActive`/`ajaxRefreshTransit`/`draw.*` event'i
  otomatik kaydediliyor (`recordEventIfEnabled()`). Playback'te video oynatılırken event log
  `video.currentTime`'a göre senkron "replay" ediliyor — ama bu turda **tam chart re-render**
  değil, okunabilir bir "Etkinlik Günlüğü" (metin listesi, örn. "12:34 Harita değiştirildi:
  Solar Return") olarak gösteriliyor; bkz. §9'daki kapsam notu.
  Video dosyası `storage/app/private` (local disk, web'den DOĞRUDAN erişilemez) altında saklanıyor,
  erişim SADECE `RecordingController::video()` üzerinden katılımcı doğrulamasıyla mümkün; video
  seeking (scrub) için `BinaryFileResponse` + HTTP Range desteği elle eklendi (Laravel'in
  `Storage::response()` varsayılanı Range desteklemiyor — bu turda bulunup düzeltilen gerçek
  bir bug, bkz. §8).
- **Faz 7 — reconnect:** Ekstra kod YAZILMADI çünkü mimari zaten kapsıyor — doğrulama için
  `laravel-echo`'nun kaynağına bakıldı: `channel.subscribed(cb)` sadece ilk bağlantıda değil,
  **her başarılı (yeniden) subscribe'da** tetikleniyor (`node_modules/laravel-echo/dist/echo.js`).
  pusher-js zaten bağlantı koptuğunda otomatik reconnect + tüm kanallara yeniden subscribe
  yapıyor; bu da öğrencinin `request_sync`'ini otomatik tekrar tetikliyor → öğretmen otomatik
  `full_sync` gönderiyor. Yani bağlantı kopup gelen bir öğrenci ekstra hiçbir şey yapmadan
  senkronize oluyor.

---

## 3. Eklenen/değişen dosyalar

```
app/Events/LiveSessionEvent.php                              (yeni — tek generic event, type'a göre davranır)
app/Http/Controllers/Frontend/LiveSessionController.php       (yeni)
app/Http/Controllers/Frontend/RecordingController.php         (yeni — Faz 6, kayıt liste/playback/video/events)
app/Http/Controllers/Frontend/IndexController.php             (index() opsiyonel $liveSession parametresi — additive)
app/Models/LiveSession.php                                    (yeni + Faz 6 kayıt alanları/ilişkileri)
app/Models/SessionAnnotation.php                               (yeni — Faz 5 çizim kaydı)
app/Models/LiveSessionEventRecord.php                          (yeni — Faz 6 event-log satırı, $table='live_session_events')
app/Models/User.php                                            (isTeacher/isStudent eklendi)
config/broadcasting.php, config/reverb.php                    (yeni, Reverb kurulumu)
config/services.php                                            (metered_turn bloğu eklendi)
database/migrations/2026_08_04_051754_create_live_sessions_table.php (yeni)
database/migrations/*_create_session_annotations_table.php    (yeni — Faz 5)
database/migrations/*_add_recording_fields_to_live_sessions_table.php (yeni — Faz 6)
database/migrations/*_create_live_session_events_table.php    (yeni — Faz 6)
public/frontend/js/panel/live-session-sync.js                 (chart senkron client'ı — full_sync artık stroke'ları da taşır)
public/frontend/js/panel/live-webrtc.js                        (WebRTC client'ı + Faz 6 için getLocalStream())
public/frontend/js/panel/live-whiteboard.js                    (yeni — Faz 5, canvas overlay + senkron)
public/frontend/js/panel/live-recorder.js                      (yeni — Faz 6, MediaRecorder + upload)
resources/js/echo.js, resources/js/bootstrap.js               (window.Echo kurulumu)
resources/views/frontend/panel/panel.blade.php                (LIVE_SESSION config + script include'lar + readonly kilidi + launcher — hepsi @if(isset($live_session)) ile korumalı)
resources/views/frontend/panel/partials/live-session-controls.blade.php (yüzen video/whiteboard/kayıt host toolbar overlay'i)
resources/views/frontend/panel/partials/live-session-launcher.blade.php (yeni — normal /panel'de "Canlı Ders Başlat" + "Ders Kayıtlarım" linki)
resources/views/frontend/panel/recordings/index.blade.php     (yeni — Faz 6, "Ders Kayıtlarım" listesi)
resources/views/frontend/panel/recordings/show.blade.php      (yeni — Faz 6, playback + event günlüğü)
resources/views/frontend/panel/live-test.blade.php            (/diagnostics route'unda — izole test/demo sayfası)
routes/channels.php                                            (session.{roomId} yetkisi)
routes/web.php                                                  (panel.live.*, panel.recordings.* route'ları)
.env                                                             (REVERB_*, METERED_TURN_* — bkz. §7)
```

`public/frontend/js/panel/panel.js`'e **hiçbir satır eklenmedi/değiştirilmedi** — senkron katmanı
onun zaten global olan `setActive`/`ajaxRefreshTransit` fonksiyonlarını dışarıdan sarmalıyor
(wrap). Bu, ana panel sayfasının şu anki davranışını sıfır riskle koruyor.

`IndexController::index()`'teki tek değişiklik: sona eklenen opsiyonel `?array $liveSession = null`
parametresi. `null` iken (yani normal `/panel` isteğinde) davranış birebir eskisiyle aynı —
regresyon testiyle doğrulandı (bkz. §6.5).

---

## 4. Test hesapları

Tinker ile oluşturuldu, lokal veritabanında hazır duruyor:

| Rol | E-posta | Şifre |
|---|---|---|
| Öğretmen | `teacher.test@astrobilge.local` | `Test12345!` |
| Öğrenci | `student.test@astrobilge.local` | `Test12345!` |
| Oturuma dahil olmayan (yetkisiz senaryo testi için) | `outsider.test@astrobilge.local` | `Test12345!` |

---

## 5. Lokal ortamı ayağa kaldırma

```bash
# 1. WebSocket sunucusu
php artisan reverb:start

# 2. Uygulama sunucusu
php artisan serve

# 3. Frontend asset'leri (live-test.blade.php @vite kullanıyor)
npm run build      # veya geliştirirken: npm run dev

# 4. Yeni bir canlı ders odası oluşturmanın iki yolu var:
#    a) UI: öğretmen hesabıyla /panel'e girip sol-alttaki "🔴 Canlı Ders Başlat" butonuna basın
#       (artık gerçek bir buton var — bkz. partials/live-session-launcher.blade.php)
#    b) Tinker (hızlı test için):
php artisan tinker --execute="
\$s = App\Models\LiveSession::create(['teacher_id' => 145, 'status' => 'pending']);
echo \$s->room_id;
"
```

Sonra `http://127.0.0.1:8000/panel/live/{room_id}` adresini **iki farklı tarayıcı/sekme**
ile açıp birine öğretmen, diğerine öğrenci hesabıyla giriş yapın — **artık gerçek panel**
açılıyor (Faz 4). İzole stub-tabanlı test/demo sayfası için `/panel/live/{room_id}/diagnostics`
route'u hâlâ duruyor (bkz. §6).

### ⚠️ Bilinen ortam sorunu: port 8080 çakışması

`php artisan serve` bazen `8000` portunu dolu bulup otomatik `8080`'e kayıyor — bu da Reverb'in
portunu çalıyor ve WebSocket hiç bağlanamıyor (Reverb yerine Laravel'in kendisi cevap vermeye
başlıyor, tuhaf bir "Sayfa Bulunamadı" JSON/HTML hatası olarak görünür). Bu ortamda test
sırasında **üç kez** yaşandı (kök nedeni netleşmedi — muhtemelen VS Code/terminal ortamının
zaman zaman eski bir `serve` sürecini canlı bırakması). Kontrol/çözüm:

```powershell
Get-NetTCPConnection -LocalPort 8080 -State Listen | Select-Object OwningProcess
Get-CimInstance Win32_Process -Filter "ProcessId=<PID>" | Select-Object CommandLine
# Eğer CommandLine "-S 127.0.0.1:8080 ... server.php" ise (Reverb değilse):
Stop-Process -Id <PID> -Force
```

Sunucu kurulum aşamasında (NSSM ile Reverb'i sabit bir Windows Service yapınca) bu sorun
tamamen ortadan kalkacak.

---

## 6. Manuel test senaryoları

### 6.1 `/panel/live/{room_id}/diagnostics` — izole test/demo sayfası

Gerçek panel değil — sync katmanının doğru çalıştığını kanıtlamak için yazılmış izole bir
test ortamı (gerçek panel.js fonksiyonlarını taklit eden stub'larla). Gelecekteki fazların
(whiteboard, kayıt) regresyon testi için saklandı.

1. **Faz 1 — WebSocket smoke test:** "Test event'i gönder" butonuna basın, karşı sekmenin
   log'unda `test.ping` görünmeli.
2. **Faz 2 — chart senkron:** Öğretmen tarafında "Chart değiştir — lazy" / "— hazır"
   butonlarına basın; öğrenci tarafının log'unda `setActive` event'i ve simüle
   `window.CHARTS`/`activeKey` state'i güncellenmeli.
3. **Faz 2b — tam senkron:** Sayfayı yenileyip öğrenci tekrar bağlandığında log'da
   `request_sync` (öğretmen tarafında) ve `full_sync` (öğrenci tarafında) görünmeli —
   öğrenci daha önce kaçırdığı tüm chart'ları da alır.
4. **Faz 3 — video:** Her iki tarafta "Kamerayı başlat", öğretmen tarafında "Ara" —
   birkaç saniye içinde karşılıklı video akışı gelmeli. Aynı makinede iki sekme
   olduğu için TURN'e gerek kalmadan (localhost-localhost direkt bağlanır) çalışması
   beklenir.

### 6.2 `/panel/live/{room_id}` — gerçek panel (Faz 4)

1. İki sekmede öğretmen/öğrenci girişi yapın — sağ altta yüzen video kutusu (kamera/ara/kapat
   butonları) görünmeli.
2. Öğrenci tarafında normal panel formlarına tıklamayı deneyin — hiçbir şeye
   tıklanamamalı (`pointer-events:none` kilidi), sadece sağ alttaki video kutusu tepki
   vermeli.
3. Öğretmen tarafında normal panel gibi bir harita değiştirin (thumbnail'e tıklayın,
   transit stepper'ı kullanın vb.) — bu, `live-session-sync.js`'in gerçek `panel.js`
   fonksiyonlarını (stub değil) doğru sardığını kanıtlayan asıl test.
   **✅ Gerçek tarayıcıda doğrulandı (2026-08-04):** gerçek bir thumbnail'e tıklandığında
   network log'da `/publish` endpoint'ine gerçek bir `POST 200` isteği gözlemlendi.

### 6.3 Faz 5/6 — whiteboard ve kayıt (gerçek panelde)

1. Öğretmen host toolbar'da "✏️" (dondur) butonuna basın — chart üzerine şeffaf bir
   çizim katmanı gelir. Kalem rengini seçip fare ile çizin; öğrenci ekranında da aynı
   çizgi anlık görünmeli (bkz. `full_sync` ile geç katılan öğrenciye de aktarılır).
   **✅ Gerçek tarayıcıda doğrulandı:** synthetic pointer event'lerle çizilen bir stroke
   hem canvas'ta görsel olarak render oldu (ekran görüntüsüyle doğrulandı) hem de
   `/publish`'e `draw.stroke` tipiyle gerçek bir istek gönderdi.
2. "🧹" silgi, "↩️" geri al, "🗑️" temizle, "⬇️" PNG indir, "💾" kaydet (session_annotations'a
   yazar) butonlarını deneyin.
3. Kamerayı açtıktan sonra "⏺ Kayıt" butonuna basın — her iki tarafta "● Kayıtta" göstergesi
   yanıp sönmeli. Birkaç harita değiştirip "⏹ Durdur"a basın — video otomatik yüklenir.
4. `/panel/kayitlarim` sayfasına gidin — az önceki ders listede görünmeli. Tıklayıp açın:
   video oynatılabilmeli, video oynarken altındaki "Etkinlik Günlüğü" video zamanına göre
   sırayla dolmalı (geri sarınca da log sıfırlanıp yeniden inşa edilmeli).

### 6.4 Backend regresyon kontrolü (otomatik doğrulandı, 12/12 senaryo)

- Normal `/panel` isteği: `window.LIVE_SESSION` yok, `live-session-readonly` class yok,
  ek script yok, sayfa öncekiyle birebir aynı render oluyor. ✅
- `/panel/live/{room_id}`: öğretmende `<body>` class'ında `live-session-readonly` YOK,
  öğrencide VAR; her ikisinde de `LIVE_SESSION` config'i doğru `roomId`/`role`/`userId`/
  URL'lerle geliyor. ✅
- Öğrenci `recording/start` deneyince 403, öğretmen 200 ✅; kayıt aktifken `setActive`
  event'i `live_session_events`'e otomatik düşüyor ✅; annotation kaydı, kayıt durdurma,
  video upload, "Ders Kayıtlarım" listesinde görünme, playback sayfası, event log JSON'ı,
  video stream (Range destekli, 206 Partial Content) ve yetkisiz kullanıcı için 403 —
  hepsi tek bir uçtan-uca script'te 12/12 doğrulandı.

---

## 7. TURN sunucusu — ücretsiz kurulum

**Neden gerekli:** WebRTC bağlantılarının ~%15-20'si (kurumsal ağlar, bazı mobil operatörler,
simetrik NAT) sadece STUN ile kurulamaz; TURN (relay) sunucusu şart olur. TURN olmadan bu
kullanıcılarda chart/sohbet çalışır ama **kamera/ses hiç gelmez**.

**Kod tarafı tamamen hazır** (`LiveSessionController::iceServers`, `live-webrtc.js`) —
API key `.env`'de boşken otomatik STUN-only'e düşer, hiçbir şey kırılmaz.

**Yapılması gereken (kod değil, kayıt):**

1. https://www.metered.ca/tools/openrelay/ adresinden ücretsiz hesap açın (kredi kartı istemez).
2. Dashboard'dan bir "App" oluşturup **App name** ve **API key**'i alın.
3. `.env` dosyasında doldurun:
   ```
   METERED_TURN_APP_NAME=sizin-app-adiniz
   METERED_TURN_API_KEY=sizin-api-keyiniz
   ```
4. Ücretsiz kota: ayda 20GB TURN relay + 100 eşzamanlı bağlantı, SLA garantisi yok ama
   başlangıç için yeterli (rapor Bölüm 5.5'teki "$0 kurulum" kararıyla tutarlı).

Girildikten sonra hiçbir kod değişikliği gerekmez — `iceServers()` endpoint'i otomatik
devreye girer (12 saatlik cache ile).

---

## 8. Bu oturumda bulunup düzeltilen gerçek bug'lar

Tüm fazlar boyunca (Faz 1 → Faz 6) test ederken bulunan, kod tarafında düzeltilen gerçek
hatalar — ileride benzer bir entegrasyon yapılırsa aynı tuzaklara düşmemek için:

1. **`window.Echo` hiç kurulmuyordu (Faz 4):** `panel.blade.php` normalde Vite kullanmıyor,
   `@vite(['resources/js/app.js'])` etiketi hiç yoktu. Ek olarak `@vite`'ın ürettiği
   `type="module"` script'i DEFERRED çalışır, oysa `live-session-sync.js`/`live-webrtc.js`
   sıradan senkron `<script src>` olarak echo.js'den ÖNCE çalışıyordu. **Düzeltme:**
   panel.blade.php'ye koşullu `@vite` eklendi + tüm client modülleri `DOMContentLoaded`'a
   ertelendi (spec gereği module script'ler her zaman DOMContentLoaded'dan önce tamamlanır).
   Gerçek tarayıcıda doğrulandı: gerçek bir chart thumbnail'ine tıklanınca network log'da
   `/publish`'e gerçek POST (200) gözlemlendi.
2. **`LiveSessionEventRecord` modeli yanlış tabloya bakıyordu (Faz 6):** Eloquent'in sınıf
   adından otomatik türettiği tablo adı (`live_session_event_records`) migration'daki gerçek
   adla (`live_session_events`) eşleşmiyordu — `SQLSTATE[42S02]: table not found` hatası
   veriyordu. Düzeltme: modele `protected $table = 'live_session_events';` eklendi.
3. **Video seeking/scrub çalışmıyordu (Faz 6):** `Storage::disk('local')->response()`
   Laravel'de `StreamedResponse` döner, bu HTTP Range isteklerini DESTEKLEMEZ (video
   player'da ileri/geri sarma çalışmaz). Düzeltme: `RecordingController::video()`
   `Symfony\Component\HttpFoundation\BinaryFileResponse` + `->prepare($request)` kullanacak
   şekilde yeniden yazıldı — gerçek testte `Range: bytes=100-199` isteğine artık doğru
   `206 Partial Content` + `Content-Range` dönüyor.
4. **Test script'lerinde (uygulama hatası değil) `/panel`'in 5 dakikalık kullanıcı-bazlı
   HTML cache'i** (`IndexController::index()`) bazen test script'lerimin eski/geçersiz bir
   `csrf-token` yakalamasına yol açtı (419 hatası) — çözüm olarak cache'siz
   `/panel/csrf-token` endpoint'i kullanıldı. Bu, gerçek kullanıcı akışını etkilemez
   (tarayıcıda sayfa yenilenmeden CSRF token değişmez), sadece test script'i tuzağıydı.
5. **Lokal ortamda tekrarlayan port 8080 çakışması** (3 kez yaşandı) — `php artisan serve`
   bazen 8000'i dolu bulup 8080'e (Reverb'in portu) kayıyor. Kök neden netleşmedi (muhtemelen
   VS Code/terminal ortamının eski bir `serve` sürecini bazen canlı bırakması); üretimde
   sorun olmayacak çünkü Reverb NSSM ile ayrı bir Windows Service olacak (bkz. §5'teki uyarı).

---

## 9. Açık sorular / kapsam dışı bırakılanlar

Bunlar kod eksikliği değil, **ürün/tasarım kararı gerektiren** ya da bilinçli olarak
kapsam dışı bırakılmış noktalar:

- **Slot/düzen senkronu hâlâ yok:** Öğretmenin ekranındaki TÜM açık haritalar (içerik +
  bilgileri) öğrenciye gidiyor (`full_sync`), ama hangi chart'ın sol/sağ panelde hangi
  sırada durduğu (`panelSlotState`/Sortable.js düzeni) henüz senkronize edilmiyor — sadece
  aktif (main) chart ve tüm `CHARTS` verisi. Rapor Bölüm 3.6'da da bu ayrı, daha zor bir
  alt problem olarak işaretlenmişti.
- **Playback'te tam chart re-render yok (Faz 6, bilinçli kapsam kararı):** Ders kaydı
  izlenirken chart'lar video içinde görselleşmiyor — bunun yerine "Etkinlik Günlüğü" adında
  okunabilir bir metin listesi (örn. "12:34 Harita değiştirildi: Solar Return") video ile
  senkron akıyor. Tam pixel-perfect chart replay istenirse, playback sayfasına panel.js'in
  kendisini (CHARTS bootstrap dahil) yüklemek gerekir — bu, mevcut mimariden çok daha büyük
  bir iş, ayrı bir faz olarak planlanmalı.
- **Kayıt rızası (consent) diyaloğu yok:** Şu an öğretmen "⏺ Kayıt" butonuna basınca kayıt
  hemen başlıyor, sadece "● Kayıtta" göstergesi (her iki tarafta) görünür oluyor. Rapor
  Bölüm 11.6'daki "kayıt başlamadan önce onay/bildirim" akışı (ör. bir modal, öğrencinin
  reddetme hakkı) henüz yok — KVKK/GDPR açısından netleşmesi gereken bir ürün kararı.
  Ayrıca **saklama/silme politikası** (kaç ay tutulacak, kullanıcı silme talep edebilecek mi)
  hiç uygulanmadı.
- **Mobil tasarım:** Video/whiteboard/kayıt toolbar'ı (`live-session-controls`) sabit
  220px genişlikte, masaüstü için tasarlandı. Küçük ekranlarda üst üste binme/taşma riski
  var — rapor Bölüm 4.3'te de "mobil için ayrı bir strateji gerekir" diye işaretlenmişti,
  tasarım girdisi olmadan tahmin ederek değiştirmedim.
- **Reconnect — kod yazılmadı, zaten çalışıyor:** `laravel-echo`'nun kaynağı incelendi:
  `channel.subscribed(cb)` her başarılı (yeniden) subscribe'da tetikleniyor, pusher-js
  bağlantı koptuğunda otomatik reconnect + resubscribe yapıyor → öğrencinin `request_sync`'i
  otomatik tekrar tetiklenip öğretmen otomatik `full_sync` gönderiyor. Ekstra kod gerekmedi.
- **"İki gerçek tarayıcı yan yana" görsel testi hâlâ yapılmadı:** Paylaşılan tek tarayıcıda
  tüm sekmeler aynı çerezi paylaştığı için öğretmen ve öğrenciyi aynı anda iki sekmede tutmak
  mümkün olmadı. Bunun yerine zincir parça parça doğrulandı (gerçek panel.js'in doğru
  publish ettiği + backend'in doğru broadcast ettiği + öğrenci mantığının doğru replay
  ettiği — üçü birlikte sistemin çalıştığını kanıtlıyor). İki farklı tarayıcı/profil ile
  bizzat denemek isterseniz roomId'yi paylaşabilirim.
- **Sunucu kurulumu (NSSM, ARR reverse proxy, coturn):** Kullanıcı isteğiyle bilinçli olarak
  en sona bırakıldı, ayrıca anlatılacak.
- **Otomatik test scriptleri kalıcı değil:** Bu fazlar boyunca kullanılan Node tabanlı
  E2E/unit test script'leri (login simülasyonu, gerçek WebSocket bağlantısı,
  `RTCPeerConnection`/`MediaRecorder` mock'ları — toplam 21 test senaryosu, hepsi geçti)
  session'ın geçici scratchpad dizininde yazıldı, repoya commit edilmedi. İstenirse
  bunlar `tests/` altına kalıcı hale getirilebilir.
- **Test hesapları veritabanında kalıcı:** `teacher.test@astrobilge.local`,
  `student.test@astrobilge.local`, `outsider.test@astrobilge.local` — bunlar prod'a
  deploy edilmeden önce silinmeli (veya prod migration'da hiç seed edilmemeli, sadece
  lokal `astro_bilge_local` veritabanında var).

---

## 10. Rework (2026-08-04, 2. tur) — plan uyumu + modern UI + kritik bug fix'leri

Kullanıcı geri bildirimi: "entegrasyon güzel görünüyor ama işlevsellik düzgün
çalışmıyor; kameralar sol üst sub-chart'ın yerine gelmeliydi; UI modernleşmeli."
Plan dosyası baştan okundu, eksik/hatalı yerler tespit edilip düzeltildi.

### 10.1 Kamera artık sol üst sub-chart slotunda (rapor Bölüm 4 — önceden hiç yapılmamıştı)

- **Yeni dosya:** `public/frontend/js/panel/live-video-slot.js`
- Karşı tarafın kamerası, `#leftList`'teki İLK thumb-item'ın ekran koordinatlarına
  hizalanan bir overlay'i kaplar; kendi kameran sağ altta küçük PiP kutusunda (aynalı).
- Rapor 4.1'e uygun: video elementi thumb listesinin İÇİNE konmaz (Sortable/lazy-load
  DOM'u bozar, MediaStream kopar) — body'ye bir kez eklenir, konum takibi
  ResizeObserver + resize/scroll + 500ms periyodik kontrolle yapılır.
- `#leftList` görünmüyorsa (mobil) otomatik sağ-alt yüzen PiP moduna düşer.
- Üzerinde: rol rozeti, CANLI/BAĞLANIYOR/KAMERA İZNİ YOK durum çipi, "● KAYIT" rozeti.

### 10.2 WebRTC baştan yazıldı — "görüntü gelmiyor" bug'larının kök nedenleri

Eski akış: kullanıcı "Kamerayı başlat"a, öğretmen sonra "Ara"ya basmak zorundaydı ve:
1. Öğrenci kamerasını offer'dan SONRA açarsa track'ler bağlantıya hiç eklenmiyordu
   (renegotiation yok) → öğretmen öğrenciyi hiç göremiyordu.
2. Öğretmen "Ara"ya öğrenci kanala girmeden basarsa offer kayboluyordu (retry yok).
3. Remote description'dan önce gelen ICE adayları düşürülüyordu.

Yeni akış (`live-webrtc.js`):
- Sayfa açılınca kamera OTOMATİK istenir (Meet/Zoom gibi); reddedilirse izleyici
  modunda devam (recvonly transceiver'lar).
- **Tek yönlü offerer modeli:** sadece öğretmen offer üretir (glare imkânsız).
  Her iki taraf abone olup medya hazırlığı bitince `ready` sinyali yayınlar;
  öğretmen `ready` görünce otomatik offer atar. "Ara" butonu kaldırıldı —
  bağlantı hangi taraf önce girerse girsin otomatik kurulur.
- Kamera sonradan açılırsa: replaceTrack/addTrack + öğretmende onnegotiationneeded,
  öğrencide yeniden `ready` → yeni offer. Geç kamera artık çalışıyor.
- ICE aday kuyruğu: remote description gelmeden ulaşan adaylar bekletilir.
- Backend: `signal()` validasyonuna `ready` kind'ı eklendi.

### 10.3 full_sync 413 patlaması (kritik — "öğrenci ekranı hiç senkronlanmıyor")

- `full_sync` TÜM `window.CHARTS`'ı (SVG + tab HTML'leriyle) tek pakette yayınlıyordu
  → ~7MB, Reverb limiti (2MB) aşılıyor, broadcast **413 / cURL 56** ile patlıyor,
  publish 500 dönüyordu. Yani öğrenci katıldığında öğretmenin ekranı ASLA kopyalanamıyordu.
- Fix 1: chunk'lama — `full_sync.chart` (harita başına ayrı event) × N + `full_sync.meta`
  (activeKey/keys/strokes/frozen). Öğrenci meta'yı alınca aktif chart gelene kadar
  150ms aralıkla bekleyip `setActive` uygular.
- Fix 2: tek chart bile büyük olabildiği için (natal: tabs 1.76MB + svg 1.58MB = 3.6MB)
  `.env`'de `REVERB_MAX_REQUEST_SIZE` ve `REVERB_APP_MAX_MESSAGE_SIZE` **10.000.000**'a
  çıkarıldı. ⚠️ Prod'da NSSM servisinin env'inde de aynı değerler olmalı.
- Gelecek optimizasyonu (rapor §3.3): chart'ı hiç göndermeyip öğrencinin kendi
  fetch'iyle deterministik yeniden üretmesi — şimdilik kapsam dışı.

### 10.4 Broadcast echo bug'ı (öğretmenin çizimleri kayboluyordu)

- Reverb, HTTP `publish` ile yayınlanan event'i YAYINCININ KENDİSİNE de geri
  gönderir (WS üzerinden publish edilmediği için `toOthers` mümkün değil).
- `live-whiteboard.js`'te `.draw.clear`/`.draw.undo`/`.full_sync.meta` dinleyicileri
  isHost guard'sızdı → öğretmen kendi echo'sunu uygulayıp state'ini eziyordu
  (undo çift pop; bayat meta echo'su güncel stroke'ları sıfırlıyordu — testte
  bire bir yakalandı). Hepsine guard eklendi. `live-webrtc.js` zaten
  `from_user_id` guard'lı, `live-session-sync.js`'te dinleyiciler role göre ayrık.

### 10.5 Bayat CSRF / HTML cache fix'i ("butonlar sessizce ölüyor")

- Canlı ders sayfası HTML'i artık HİÇ cache'lenmiyor (`IndexController::index`,
  `$liveSession` set ise remember atlanır) — içine gömülen `csrf_token()` bayatlayınca
  tüm publish/signal POST'ları 419 alıyordu.
- Launcher'daki "Canlı Ders Başlat" formu (normal panelin 5dk cache'i içinde kalıyor)
  artık submit anında `/panel/csrf-token`'dan TAZE token çekip öyle gönderiyor.

### 10.6 Senkron sağlamlaştırma

- Host artık her (yeniden) abone oluşunda proaktif `full_sync` yayınlar —
  öğretmen sayfayı yenilerse öğrenciler otomatik resync olur.
- Whiteboard `full_sync.meta`'sı öğrencide dondurmayı iki yönlü eşitler
  (eski sürüm sadece dondurabiliyor, çözemiyordu).

### 10.7 Modern UI

- **Kontrol çubuğu** (`partials/live-session-controls.blade.php`): alt-orta, Meet
  tarzı koyu/blur pill bar — rol rozeti, mikrofon/kamera toggle (SVG ikonlu,
  kapalıyken kırmızı), öğretmende kalem/kayıt/davet-linki, kırmızı sonlandır butonu,
  tooltip'ler, `alert()` yerine toast. Dondurma aktifken üstte çizim araç seti açılır
  (renk, silgi, geri al, temizle, PNG indir, kaydet).
- **Davet linki kopyala** butonu eklendi (öğretmenin öğrenciye link gönderme yolu yoktu).
- **Launcher** (`partials/live-session-launcher.blade.php`): koyu/blur kart butonlar,
  nabız animasyonlu kırmızı "Canlı Ders Başlat".

### 10.8 Bu turda yapılan testler (hepsi geçti)

1. Tarayıcı (öğretmen): slot ilk thumb ile piksel piksel hizalı; kamera izni yokken
   izleyici modu + "KAMERA İZNİ YOK" çipi; freeze + çizim → publish 200.
2. E2E (`scratchpad/e2e-rework-test.mjs`, script öğrenci + GERÇEK öğretmen sekmesi):
   request_sync → `full_sync.chart × 10` + `full_sync.meta` (activeKey/strokes/frozen
   doğru) alındı; öğrenci `ready` → öğretmenden OTOMATİK `webrtc.offer` + 4 ICE adayı
   geldi; öğrenci `setActive` publish → 403; geçersiz signal kind → 422.
3. Öğrenci sayfası markup: readonly kilit var, rol student, öğretmen-butonları yok.
4. Normal `/panel` regresyonu: chart render + launcher var, canlı ders artığı yok.

---

## 11. Öğretmen Paneli (2026-08-04, 3. tur)

Avatar menüsüne (header + panel varyantları, sadece `isTeacher()` kullanıcılarda)
**"Öğretmen Paneli"** öğesi eklendi → `/panel/ogretmen` (`panel.teacher`).

### Dosyalar

```
app/Http/Controllers/Frontend/TeacherPanelController.php   (yeni)
resources/views/frontend/panel/teacher/index.blade.php     (yeni)
resources/views/components/user-avatar.blade.php           (menü öğesi, iki varyanta)
resources/views/frontend/panel/partials/live-session-launcher.blade.php (panel linki)
routes/web.php                                             (panel.teacher.* route'ları)
```

### Özellikler

- **İstatistik kartları:** toplam ders, aktif ders, kayıtlı ders, toplam kayıt süresi,
  farklı öğrenci sayısı, çizim sayısı.
- **Yeni Canlı Ders Başlat:** hero'daki buton `panel.live.store`'a POST eder, doğrudan
  canlı odaya yönlendirir (bu sayfa cache'lenmediği için @csrf güvenli).
- **Aktif / Bekleyen Dersler:** rozet (öğrenci katıldıysa CANLI, yoksa "Öğrenci
  bekleniyor" — status'a değil `student_id`'ye bakar çünkü öğretmen odaya girince
  status zaten 'active' oluyor), Derse Gir, Davet Linki kopyala, **Dersi Bitir**.
- **Dersi Bitir** (`panel.teacher.session.end`): status=ended + `session.ended`
  broadcast'i — odada bağlı taraflar alert ile normal panele yönlendirilir
  (`live-session-sync.js` '.session.ended' dinleyicisi). Bitmiş odaya `join` artık
  **410 Gone** döner (eski davet linkleri ölür).
- **Geçmiş Dersler:** tarih/öğrenci/süre/kayıt tablosu; kayıt varsa İzle (playback) +
  **Kaydı Sil** (`panel.teacher.recording.delete` — video dosyası + event log silinir,
  ders satırı kalır; KVKK silme talebinin öğretmen tarafı).
- **Kaydedilmiş Çizimler:** PNG önizlemeli grid (`panel.teacher.annotation.png` —
  private diskten yetki kontrolüyle stream; PNG'siz kayıtlarda "Önizleme yok"),
  çizim silme (`panel.teacher.annotation.delete`).
- Tüm aksiyonlarda çift yetki kontrolü: sayfa `isTeacher()`, satır bazında
  `teacher_id === auth id`.

### Testler (hepsi geçti)

- Öğretmen: sayfa render (hero + 6 stat + üç bölüm), panelden ders başlat → canlı
  odaya redirect (LIVE_SESSION teacher + video slot), Dersi Bitir → flash + DB'de
  `ended` + odaya join **410**.
- Öğrenci: `/panel/ogretmen` → **403**; avatar menüsünde öğe öğrenciye görünmüyor
  (isTeacher guard).
- Avatar menüsünde "Öğretmen Paneli" öğesi ekran görüntüsüyle doğrulandı.
- Not: lokaldeki 12 eski test dersi tinker ile toplu `ended` yapıldı (panelde
  "aktif" görünüp kalabalık yapıyorlardı).

---

## 12. Whiteboard × tam ekran entegrasyonu (2026-08-04, 4. tur)

Kullanıcı geri bildirimi: (a) çizim modunda haritaya çizince harita tam ekran
açılıyordu; (b) harita tam ekrandayken alttaki bardan çizim modu açılınca tam
ekranda çizilebilmeliydi.

### (a) Bug fix — çizim tıklaması lightbox'ı tetikliyordu

`#mainDropzone`'un click handler'ı haritayı Fancybox lightbox'ında tam ekran
açıyor (`panel.js` `openLightboxForActive`). Whiteboard canvas'ı dropzone'un
çocuğu olduğu için çizim pointer/click event'leri dropzone'a KABARCIKLANIYOR ve
her fırça darbesi lightbox'ı açıyordu. Fix: canvas'taki `pointerdown/move/up` +
`click` event'lerinde `stopPropagation` (+`preventDefault`) — dondurma modunda
canvas tüm yüzeyi kapladığı için chart etkileşimleri zaten bilinçli olarak kesiliyor.

### (b) Özellik — tam ekranda çizim

- Whiteboard artık dinamik konteyner destekliyor: normalde `#mainDropzone`,
  Fancybox modal'ı açıkken `.astro-modal__dropzone`. TEK canvas elementi
  `appendChild` ile taşınır; `MutationObserver(document.body, {childList, subtree})`
  modal aç/kapa anlarını yakalar (`ensureAttached()`).
- Stroke'lar normalize (0-1) koordinat tuttuğu için canvas hangi boyuta taşınırsa
  taşınsın çizimler doğru orantıda yeniden çizilir — tam ekranda çizilen stroke,
  öğrencinin normal boyuttaki ekranında da doğru yerde görünür (ve tersi).
- Modal kapanınca canvas otomatik normal dropzone'a döner (JS referansı bizde
  olduğu için Fancybox DOM'u yok etse bile canvas ve stroke'lar yaşar).
- `#liveVideoSlot` z-index 900 → **1100** (Fancybox ~1050'nin üstü): harita tam
  ekrandayken kameralar PiP gibi görünmeye devam eder (rapor 10.2: kamera
  dondurmadan etkilenmemeli ilkesinin tam ekran hali). Kontrol barı zaten 2100.

### Testler (tarayıcıda, hepsi geçti)

1. Dondur + çiz → stroke çizildi, lightbox AÇILMADI (fix öncesi açılıyordu).
2. Çöz + haritaya tıkla → lightbox normal açıldı; MutationObserver canvas'ı
   modal'a anında taşıdı; bardan dondur → canvas modal boyutunda (980×715)
   görünür oldu, tam ekranda stroke çizildi, modal açık kaldı.
3. Ekran görüntüsü: çizimler tam ekran haritada, kamera slotu modal üstünde,
   kontrol barı erişilebilir.
4. Modal kapat → canvas `#mainDropzone`'a geri döndü, 2 stroke ve frozen durumu
   korundu.

---

## 13. Admin yetkilendirme + role göre çıkış yönlendirmesi (2026-08-04, 5. tur)

### Öğretmenlik yetkisi artık admin panelinden veriliyor

- `admin/users` tablosuna **"Öğretmen"** sütunu eklendi: yeşil "Öğretmen" rozeti
  (tıklayınca yetkiyi kaldırır) veya "Yetki Ver" butonu — ikisi de confirm'li.
- Endpoint: `POST /admin/users/{user}/toggle-teacher`
  (`Admin\UserController::toggleTeacher`, `admin.auth` middleware'i arkasında) —
  `users.role`'ü `user` ↔ `astrologer` arasında çevirir. `User::isTeacher()` bu
  alana baktığı için canlı ders başlatma, Öğretmen Paneli sayfası ve avatar
  menüsündeki öğe otomatik olarak açılıp kapanır. Kullanıcı kendi kendine
  öğretmen OLAMAZ — tek yol admin.
- Test: rol çevirme mantığı tinker ile round-trip doğrulandı
  (user → astrologer → user); blade compile + route kaydı doğrulandı.
  (Admin UI'ın kendisi admin şifresi gerektirdiği için tarayıcı E2E'si yapılmadı.)

### "Görüşmeyi sonlandır" — role göre yönlendirme

- Yeni endpoint: `POST /panel/live/{roomId}/end` (`LiveSessionController::end`,
  sadece öğretmen) — dersi kalıcı bitirir + `session.ended` broadcast eder.
- `join()` artık `end_url` ve role göre `exit_url` üretir:
  **öğretmen → `/panel/ogretmen` (Öğretmen Paneli), öğrenci → `/panel`**.
- Kontrol çubuğundaki kırmızı buton: öğretmende confirm → end_url POST →
  Öğretmen Paneli'ne yönlenir; öğrencide sadece görüşmeden ayrılıp normal
  panele döner (ders açık kalır, öğrenci linkle geri girebilir).
- `live-session-sync.js` `.session.ended` dinleyicisi: karşı taraf alert +
  KENDİ exit_url'üne yönlenir (sonlandıran taraf `__liveSessionSelfEnded`
  bayrağıyla çift alert'ten muaf). Öğretmen Paneli'ndeki "Dersi Bitir" de aynı
  event'i yayınladığı için oradan bitirilen derste de aynı yönlendirme çalışır.
- Test (tarayıcı): öğretmen ders açtı → sonlandır butonu → DB'de `ended` +
  öğretmen `/panel/ogretmen`'e yönlendi. Öğrenci join HTML'inde
  `exitUrl=/panel` + student için de doğrulandı.

---

## 14. Prod kurulum scripti (2026-08-04)

Canlı sunucu (Windows Server + Plesk + IIS) kurulumu için hazır script:

```
deploy/canli-ders-kurulum.bat   ← SUNUCUDA yönetici olarak çalıştırılacak olan
deploy/canli-ders-kurulum.ps1   ← asıl kurulum mantığı (.bat bunu çağırır)
```

**Kullanım:** Proje dosyaları sunucuya çekildikten sonra `deploy\canli-ders-kurulum.bat`
sağ tık → "Yönetici olarak çalıştır". Script sırasıyla: ortam kontrolü →
composer/npm install → `.env` güncellemesi (Reverb anahtarları, 10MB limitler,
`VITE_REVERB_HOST=<domain>`/443/https, opsiyonel Metered TURN — domain'i ve TURN
anahtarlarını sorar) → `npm run build` (VITE değerleri build'e gömüldüğü için
.env'den SONRA) → `migrate --force` → cache (route:cache bilinçli atlanır,
closure route'lar var) → NSSM indirip Reverb'i `AstroBilgeReverb` Windows
Service olarak kurar (otomatik restart, log: `storage/logs/reverb-service.log`)
→ IIS WebSocket özelliğini açar, ARR server-proxy'yi etkinleştirir ve
`public/web.config`'e `^app/(.*) → http://127.0.0.1:8080/app/{R:1}` reverse-proxy
kuralını (yedek alarak) enjekte eder → doğrulama + son kontrol listesi.

**Notlar:**
- Script **idempotent** — tekrar çalıştırmak güvenli (mevcut anahtarları korur,
  servis varsa ayarlarını tazeler, kural varsa eklemez).
- URL Rewrite ve ARR **MSI** oldukları için otomatik kurulMAZ; eksikse script
  indirme linklerini gösterir, modüller kurulduktan sonra script tekrar çalıştırılır.
- `.ps1` **UTF-8 BOM'lu** kaydedildi — Windows PowerShell 5.1 BOM'suz UTF-8'i
  ANSI okuyup em-dash gibi karakterleri akıllı-tırnağa çevirerek parse hatası
  veriyor (lokalde PS 5.1 parser'ıyla doğrulandı). Dosya yeniden kaydedilirse
  BOM korunmalı.
- Kod güncellemelerinde deploy sonrası: `composer install` + `npm run build` +
  `migrate --force` + `config:cache` + `nssm restart AstroBilgeReverb`.

---

## 15. Sync v2 — "öğrenci kendi haritasını görüyor" bug'ının kökten çözümü (2026-08-05)

Kullanıcı raporu: "Öğretmenin main'inde Uranyen varsa öğrencide de Uranyen
açılıyor ama İÇERİK öğrencinin kendi Uranyen'i, öğretmeninki değil."

### Kök neden — mimari varsayım yanlıştı

Rapor §3.2'nin "parametre replikasyonu" ilkesi ("öğrenci aynı parametreyle
kendi fetch'ini atar, deterministik aynı sonucu alır") yanlış bir varsayıma
dayanıyordu: panelin TÜM fetch uçları (LOAD_CHART_LAZY_URL, CHART_TABS_URL,
TRANSIT_REFRESH_URL) chartForm/querystring üzerinden GİRİŞ YAPMIŞ KULLANICININ
KENDİ doğum verisini gönderir. Yani öğrencinin "aynı parametreli" fetch'i
kendi natal'ına göre üretilen farklı bir harita döndürür. Üç sızıntı yolu:
lazy SVG fetch (boot yarışı + lazy chart aktivasyonu), sağ-tab fetch'i,
transit refresh replay'i.

### Çözüm — rapor §3.3 alternatifi: SONUÇ replikasyonu

`live-session-sync.js` v2 olarak yeniden yazıldı:

- **Öğrenci = saf ayna:** `loadChartSvgLazy` / `ensureRightTabsLoaded` /
  `forceRefreshActiveChartSvg` öğrencide no-op'lanır (top-level function
  declaration'lar window üzerinden override edilebiliyor; panel.js içindeki
  nitelenmemiş çağrılar da global binding üzerinden override'a düşer).
  Ekrandaki her şey öğretmenden gelen chart objesinden render edilir;
  `applyHostChart()` thumbnail'i de günceller (eskiden thumbnail'ler
  öğrencinin server-side render'ında kalıyordu).
- **Host setActive:** yayın, chart'ın svg VE tabs'ı hazır olana kadar bekler
  (40×250ms; eski 20×150ms sadece lazySvg'ye bakıyordu) — öğrenci artık
  kendisi tamamlayamayacağı için eksik chart gitmemeli.
- **Host ajaxRefreshTransit:** parametre yayını KALDIRILDI; fetch bitince
  window.CHARTS'ta svg'si değişen/eklenen chart'lar `chart_replace` event'iyle
  (sonuç olarak) yayınlanır. Değişiklik tespiti svg string referans
  karşılaştırmasıyla (ucuz).
- **Host full_sync:** chunk'lamadan önce lazy kalmış chart'lar
  `loadChartSvgLazy` ile yüklenir — öğrencinin thumbnail'leri dahil her şey
  öğretmenin verisi olur.
- **Backend:** `chart_replace` RECORDABLE listesine eklendi;
  `recordEventIfEnabled` artık chart payload'ını log'a yazmadan önce küçültür
  (sadece title — yoksa her harita geçişi DB'ye multi-MB satır yazardı).

### E2E doğrulama (script öğrenci + gerçek öğretmen sekmesi)

- full_sync: **20/20 chunk gerçek svg'li** (lazy-preload öncesi 6/10'du),
  18/20 tab'lı, meta activeKey doğru.
- Öğretmen `setActive('draconic')` → öğrenciye svg+tabs DOLU tek event.
- Öğretmen `ajaxRefreshTransit('2026-09-15', ...)` → öğrenciye
  `chart_replace(natal_transit)` svg'li — parametre değil sonuç gitti.

### Bilinen küçük sınırlar

- Öğrencinin transit stepper'ındaki tarih ETİKETİ senkronlanmıyor (kozmetik;
  chart içeriği doğru — setLabel closure-içi, erişilemiyor).
- Öğrenci boot'unda full_sync gelene kadar (~5-10 sn) kısa süre kendi
  haritası görünür (bilinen MVP davranışı, §2'de zaten kayıtlı).
- panel.js'in natal-tab preloader'ı (satır ~3170) boot anında çalıştığı için
  override'dan önce fetch başlatabilir; full_sync sonrası öğretmen verisi
  üzerine yazdığından pratikte etkisiz (yalnızca sıra dışı yavaş full_sync +
  hızlı preloader kombinasyonunda kısa süreli yanlış natal tab olasılığı).

### §15 ek — bilgi kutuları (infobox) senkronu (2026-08-05)

- `chart.tabs` zaten 9 kutuyu taşıyor ve v2 ile senkron: aspect, planets,
  firdaria, strong, dominant, **elements, qualities, chakras, polarity**
  (updateRightTabs bunları CHARTS[key].tabs'tan basıyor) + stellium kutusu ve
  ev sistemi etiketi `CHARTS[key].visual`'dan → otomatik senkron.
- İKİ İSTİSNA tespit edilip full_sync.meta'ya `uiBoxes` alanıyla eklendi:
  **#chartMeta** (isim + doğum tarihi/saat/konum kimlik kartı — öğrenci
  öğretmenin haritası üstünde KENDİ adını görüyordu) ve **#moonPhaseBox**
  (Ay Fazı — updateRightTabs listesinde yok). Öğrenci meta gelince ikisinin
  innerHTML'ini öğretmeninkiyle değiştirir (setActive'ten önce uygulanır).
- E2E: 10/10 chunk svg'li, 9/10 tab'lı; meta'da chartMeta öğretmene ait
  ("Test Ogretmen" içeriyor) + moonPhaseBox dolu — doğrulandı.

---

## 16. Çizim kaydetme — birleşik anlık görüntü (2026-08-05)

Kullanıcı raporu: "Çizimi kaydedince sadece çizgiler kaydediliyor; hangi
haritanın üstüne çizdiysem o harita + info box + çizgiler kaydedilmeli."

`live-whiteboard.js`'e `composeSnapshotDataUrl()` eklendi (rapor §10.5'in
gerçek uygulaması) — İndir ve Kaydet artık şunu üretir:
beyaz zemin + **haritanın SVG'si** (ekrandaki konumuna) + **bilgi kartı**
(harita başlığı + #chartMeta kimlik satırları, canvas'a metin olarak çizilir)
+ **çizgiler**. Tam ekran modunda çizildiyse modal'daki SVG kullanılır.
Kaydet butonu async oldu (kaydediliyor…/kaydedildi/kaydedilemedi toast'ları).

SVG rasterizasyonunda üç gerçek engel çıktı, üçü de çözüldü:
1. **Simsiyah çark:** Chart SVG'si stillerini sayfa CSS'inden (class) alıyor;
   bağımsız serialize edilince stiller kaybolup varsayılan siyah dolguya
   düşüyordu → serialize öncesi her düğümün HESAPLANMIŞ stili clone'a inline
   yazılıyor (svg-crowbar tekniği).
2. **Semboller düz harf:** Astroloji glif fontu (HamburgSymbols, @font-face)
   SVG-image sandbox'ında yüklenemiyor → font bir kez fetch edilip base64
   data-URI olarak SVG'ye <style> ile gömülüyor (promise cache'li).
3. **Hayalet gezegenler:** Gezegen yazıları beyaz halo için
   `paint-order: stroke` kullanıyor; bu özellik inline listesinde yoktu →
   beyaz kontur dolgunun üstüne binip glifleri siliyordu → `paint-order` (+
   `stroke-miterlimit`) inline listesine eklendi.

Test: canlı derste dondur+çiz+kaydet → üretilen PNG canlı haritayla birebir
(renkli glifler, dereceler, eksenler, açı çizgileri, kimlik kartı, çizim) —
ekran görüntüsüyle doğrulandı. Bozuk ara-deneme annotation'ları (id 1-3)
silindi.
