# Gmail INBOX entegrasyonu

`new-week` sayfasındaki **INBOX** kolonu, kullanıcının kendi Gmail gelen kutusunu
kompakt kartlar halinde gösterir. Bir kartı hafta günlerinden birine sürüklemek o
güne bir `Task` + kaynağı Gmail olan bir `Note` ekler.

Akış: `Gmail API (read-only) → google_accounts / gmail_messages → App\Livewire\Inbox → kart → NewWeek::createTaskFromEmail()`

Kolondaki veriler **her zaman yerel tablolardan** okunur; sayfa render'ı Google'a
istek atmaz. Google'a giden tek yollar `gmail:sync` komutu ve "Yenile" butonudur.

## Yapılandırma

`.env` üç anahtar ister (`config/services.php` → `google.*`):

| Anahtar | Değer |
|---|---|
| `GOOGLE_CLIENT_ID` | OAuth istemci kimliği |
| `GOOGLE_CLIENT_SECRET` | OAuth istemci sırrı (yalnızca `.env`, asla commit edilmez) |
| `GOOGLE_REDIRECT_URI` | **`{APP_URL}/auth/google/callback`** |

`GOOGLE_REDIRECT_URI` boş/eksikse tüm Gmail çağrıları
`Gmail entegrasyonu eksik yapılandırma: ...` hatası fırlatır.

⚠️ Şema, host ve path **birebir** eşleşmek zorundadır; `http`/`https` farkı bile
`redirect_uri_mismatch` verir. Mevcut `.env`'de bu değer yanlışlıkla sayfanın
kendi adresi (`/new-week/this_week`) olarak duruyordu — callback rotası
`/auth/google/callback`'tur.

### Google Cloud Console

1. **APIs & Services → Credentials → OAuth 2.0 Client IDs** → ilgili Web client.
2. **Authorized redirect URIs** satırına tam olarak şu eklenir:
   `https://planner.apper.com.tr/auth/google/callback`
3. **OAuth consent screen**: **Test mode** kapalı olmalı (aşağıya bak).

### Cron (isteğe bağlı ama önerilir)

`app/Console/Kernel.php` zaten `gmail:sync`'i 15 dakikada bir, çakışmasız ve
arkaplanda kuyruğa alıyor:

```php
$schedule->command('gmail:sync')->everyFifteenMinutes()->withoutOverlapping(10)->runInBackground();
```

Sunucuda tek satır crontab yeterli:

```
* * * * * cd /home/ugur-karagoz/Projects/planner.apper && php artisan schedule:run >> /dev/null 2>&1
```

Cron kurulmasa da özellik çalışır: "Yenile" butonu `SyncUserInbox`'ı istek
sırasında `budget=8` ile kendisi koşar. Cron'un farkı, sayfaya dokunmadan
kartların tazelenmesi.

## Konsol ekranı "Testing" modunda kalırsa

Refresh token **7 günde bir geçersiz** olur ve senkron sessizce durur; belirti
yalnızca kartların güncellenmemesidir. Bunu engellemek için consent ekranını
**In production**'a alın. Alinmayacaksa en azından Gmail'i **Test users**
listesine ekleyin ve şunu bilin:Bağlantı kopduğunda `google_accounts.revoked_at`
dolar ve INBOX kolonu "Yeniden bağlan" kartı gösterir — yani patlama gürültülü,
sessiz değil.

## `APP_KEY` rotasyonu

`google_accounts.access_token` ve `refresh_token` alanları `encrypted` cast'i ile
saklanır. `APP_KEY` değişirse (veya farklı ortam arasında taşınırsa) kayıtlı tüm
token'lar okunamaz hale gelir ve kullanıcıların yeniden bağlanması gerekir.
Deploy notlarına yazın; `php artisan key:generate` sonrasında
`google_accounts` tablosunu temizlemeyi planlayın.

## Komutlar

```bash
# Tüm bağlı kutuları senkronla
php artisan gmail:sync

# Tek hesap, daha büyük bütçeyle
php artisan gmail:sync --user=ben@posta.local --budget=25

# OAuth olmadan yerelde kartları görmek için sahte veri (yalnızca local ortam)
php artisan gmail:demo --user=ben@posta.local --count=6
php artisan gmail:demo --user=ben@posta.local --clear
```

`gmail:sync` çıplak `--user` ile karşılaştırmasını `google_accounts.email`
üzerinden yapar. Bir bağlantı geçersizleştiğinde komut `FAILURE` dönmez;
`Gmail hesap bağlantısı geçersiz kılınmış` uyarısını basar ve satırı
`revoked_at` ile işaretler.

`gmail:demo` gerçek bir Gmail bağlantısı olan kullanıcıya yazmayı reddeder ve
ürettiği satırları `message_id LIKE 'demo-%'` ile ayrıştırır; `APP_ENV=local`
dışında çalışmaz.

## Maliyet / kota

`messages.list` yalnızca id döndürür; From/Subject/snippet için mesaj başına bir
`messages.get` gerekir. Bu yüzden `SyncUserInbox` önce önbellekte **olmayan**
id'leri seçer ve detay çekeceklerini `budget` ile (`MAX_DETAIL_REQUESTS = 25`
üst sınırının altında) sınırlar. Liste sorgusu
`in:inbox -category:promotions -category:social` kullanır; detaylar
`format=metadata` + `metadataHeaders=From,Subject,Date` ile istenir, **gövde
hiç indirilmez**. Durağan durumda bir senkron turu = tek HTTP isteği.

## Sürükle-bırak köprüsü

INBOX, `@livewire('new-week')` ile **kardeş** DOM düğümü olduğu için NewWeek'ün
kök `x-data` kapsamına erişemez. Köprü, repodaki `window.showDragNotification`
alışkanlığını sürdüren `window.inboxDrag` globalidir ve Inbox şablonunun
`@script` bloğunda tanımlanır. Üç kritik nokta:

- `handleDrop()` başı: `emailId` okunur okunmaz temizlenir (istek sürerken ikinci
  bırakma aynı mesajı tekrar dönüştürmesin), sonra
  `$wire.call('createTaskFromEmail', emailId, targetDate)`.
- `handleTaskDrop()`: guard öne alındı — `if (window.inboxDrag?.emailId) return;`.
  `.task-drop-zone` gün `.drop-zone`'unun içinde olduğu için aksi halde e-posta
  bırakması sessizce yutulurdu.
- `handleBasketDrop()`: e-posta sepete bırakılırsa reddedilir ve highlight temizlenir.

Sunucu tarafı `NewWeek::createTaskFromEmail()` sahip kontrolü, zaten dönüşmüş
kart kontrolü ve hafta aralığı kontrolü yapar; `ConvertMessageToTask` içinde
`DB::transaction` ile `Task` + `Note` yazar ve `created_task_id` set eder.
Başarı `taskMoved` + `gmailTaskCreated`, ret `taskMoveError` bildirir; kart
listesi `created_task_id IS NULL` filtresi sayesinde kendiliğinden düşer.

`wire:poll` **yoktur**: polling sırasında DOM değişirse süren bir sürükleme
bozulur.

## Sunucuda teşhis

Bağlantının her adımı varsayılan log kanalına `[gmail]` önekiyle yazılır
(`App\Actions\Gmail\GmailLog`). Sunucuda tek komutla izlenir:

```bash
tail -f storage/logs/laravel-$(date +%Y-%m-%d).log | grep '\[gmail\]'
```

| Adım | Ne söyler |
|---|---|
| `client.not_configured` | `GOOGLE_*` anahtarlarından biri boş; `config_cached: true` ise suçlu `config:cache` |
| `oauth.redirect` | Butona basıldığında Google'a **hangi client_id / redirect_uri** gönderildi |
| `oauth.redirect.failed` | Auth URL'i hiç kurulamadı (yapılandırma / beklenmeyen durum) |
| `oauth.callback` | Google'ın geri ne döndürdüğü: `google_error`, `state_match`, `code`, `host`, `scheme`, `user_id` |
| `oauth.callback.state_mismatch` | 403'ün sebebi: oturumda state yoksa session kaybı (host/`www` farkı, çerez alanı), varsa tutarsızlık |
| `oauth.callback.denied` | Kullanıcı consent ekranında reddetti (`error=access_denied`) |
| `oauth.callback.empty_code` | Google kod göndermedi |
| `token.rejected` | Google token uç noktasının **gerçek hatası** (`redirect_uri_mismatch`, `invalid_client`, `invalid_grant`) + gönderilen `redirect_uri` |
| `token.transport_error` | Google'a hiç ulaşılamadı: dışa yönelik HTTPS kapalı, DNS ya da CA paketi sorunu |
| `oauth.callback.token` | Token yanıtının biçimi: süresi, verilen kapsam, refresh token var mı |
| `oauth.callback.no_refresh_token` | Offline access gelmedi → consent ekranı Test mode |
| `oauth.callback.connected` | Bağlantı kaydedildi (`account_id`, `email`, `expires_at`) |
| `oauth.callback.first_sync` / `_failed` | Bağlantıdan hemen sonraki ilk senkronun sonucu |
| `client.refresh` / `client.refreshed` | Access token yenileme turu |
| `client.api_error` | Gmail API'nin reddi: 401 token, 403 kota/erişim, 404 mesaj |
| `sync` / `sync.revoked` | Bir senkron turunun sayaçları / bağlantının düşmüş olması |

Gizli alanlar değerleriyle loglanmaz: `access_token`, `refresh_token`,
`client_secret`, `code`, `state`, `session` ve varyantları `len=<uzunluk>
fp=<sha256'nın ilk 8 hanesi>` biçiminde yazılır. İki satırda `fp` aynıysa aynı
değerden söz edilir; değerin kendisi hiçbir zaman diske inmez.
`tests/Feature/GoogleOAuthTest::test_bağlantı_adımları_loga_yazılır` bu
maskelemeyi zorunlu tutar.

## Sorun giderme

| Belirti | Neden / çözüm |
|---|---|
| `redirect_uri_mismatch` | `GOOGLE_REDIRECT_URI` ile Console'daki URI birebir aynı değil |
| Consent ekranı gelip "yetki kodu takas edilemedi" | `GOOGLE_CLIENT_SECRET` yanlış, ya da kod ikinci kez kullanılmış |
| Logda `token.rejected` + `unsupported_grant_type`, `error_description: "Invalid grant_type: "` | Kod takası `grant_type=authorization_code` göndermiyor. `token.rejected` satırındaki `payload_keys` alanının yokluğu bunu gösterir. `Http::fake` istek gövdesini denetlemediği için bu hata yalnızca canlıda görünür |
| Bağlantı kuruluyor ama `refresh_token` yok | `access_type=offline`/`prompt=consent` gönderiliyor; kullanıcı "İzin ver"i geçersiz kılmış olabilir → yeniden bağlan |
| 7 gün sonra senkron durdu | Consent screen hâlâ Test mode'da |
| Kartlar hiç görünmüyor, hata yok | Cron kurulu değil ve "Yenile"e basılmamış; `php artisan gmail:sync` elle koş |
| Hepsi `revoked_at`'li | Token `invalid_grant` döndü → "Yeniden bağlan" kartından tazele |
| Tüm token'lar bozuldu | `APP_KEY` değişmiş |
| Kartlar kayboluyor ama `gmail_messages` dolu | Kart bir göreve dönüştü (`created_task_id` dolu) — bu beklenen davranış |

## Test

⚠️ `php artisan test` geliştirme MySQL veritabanını **siler**: `phpunit.xml`'deki
sqlite override'ları yorum satırında ve testlerin çoğu `RefreshDatabase`
kullanıyor. Her zaman önekle koşun:

```bash
DB_CONNECTION=sqlite DB_DATABASE=:memory: vendor/bin/phpunit --filter=Gmail
DB_CONNECTION=sqlite DB_DATABASE=:memory: vendor/bin/phpunit --filter=Inbox
DB_CONNECTION=sqlite DB_DATABASE=:memory: vendor/bin/phpunit --filter=CreateTaskFromEmail
DB_CONNECTION=sqlite DB_DATABASE=:memory: vendor/bin/phpunit --filter=GoogleOAuth
```

Gmail kapsamı 34 test / 143 assertion: `tests/Unit/GmailDecodeMessageTest.php`,
`tests/Feature/{GmailSyncTest,InboxTest,CreateTaskFromEmailTest,GoogleOAuthTest}.php`.
Şema kurulumu `tests/Concerns/CreatesPlannerSchema.php`'de (bu repoda uygulama
tablolarının migration'ı yok, bu yüzden test tarafında elle inşa ediliyor).

Tüm paketi koşarsanız `Errors: 23, Failures: 7, Skipped: 7` görürsünüz. Aynı
sayılar temiz bir `HEAD` worktree'nde de çıkıyor — yani kırılmalar Gmail'den
önce de vardı (Jetstream testleri bu uygulamanın team/registration
özelleştirmeleriyle bozuk). Sonuç: tam paket yeşil sinyal vermez, `--filter`
koşun.

## Kapsam dışı

E-posta detay/gövde okuma modalı, Gmail'e yazma (okundu işaretleme, arşivleme,
silme), e-postadan otomatik `Company` eşleştirme, `history` webhook'u ile push
senkron, kişi başına birden fazla hesap.
