# Quickly POS — Çakışmasız Offline Senkron

**Türkçe** | [English](./OFFLINE-SYNC.en.md)

**Sürüm:** 2026.07.24 (POS cache **20260724cg**)  
**Amaç:** Web (Hesap) ↔ masaüstü/web POS arasında **çift yazar çakışması olmasın**.  
İnternet kesilince satış devam eder; dönünce önce outbox push, sonra katalog pull.

> Platform şema + öncelik listeleri: [`PLATFORM-SCHEMA.md`](./PLATFORM-SCHEMA.md).

> **2026.07.24 — KDS üç aşama (cg):** Satır `1` Bekleyen → `4` Hazırlanan → `2` Tamamlanan; Angular `confirmCheck` artık `1→2` yapmaz (`fired` ödeme kapısı). Peer `kitchen_ready_patch` monoton `preferKitchenStatus`; garson `staff_alert` yalnız yeni `2` (delta). E2E plane D. Cache **20260724cg**.
>
> **2026.07.23 — Pouch `auto_compaction`:** Yerel IndexedDB Pouch açılışlarında `auto_compaction: true` (özellikle `local_checks` / `local_closed_checks`). Veri silinmez; eski revizyonlar yazım sonrası sıkıştırılır — `closed_checks` büyümesini yavaşlatır. Kaynak: `main.service.ts`, `pos/m`, `catalog-bridge`, `check-device`. Cache **20260723cf**.
>
> **2026.07.23 — mPOS 429/offline chrome:** `QuicklyCheckLock.pullBackoffRemainingMs` + `quickly:pull-backoff`; sync chip «Bekleme Ns» / «Çevrimdışı · yerel»; Senkron sayfası banner («donmuş değil»). Cache **20260723cc**.
>
> **2026.07.23 — KDS mutfak «Hazır» push (D):** Yabancı / soft-upsert check’te KDS/mPOS «Hazır» → `check_upsert` + `kitchen_ready_patch` (`chkkit_<checkId>`). Sunucu satır yapısını korur, yalnızca `product.status` 1→2 ilerletir; `origin_device_id` çalınmaz. Sahip cihaz soft-merge’te kendi check’ine monoton mutfak status uygular + masa **WILL_READY(3)**. Echo yok (yabancı structural re-push hâlâ kapalı). E2E plane D: `e2e-cross-device-sync.mjs`. Cache **20260723bq**.
>
> **2026.07.23 — Kasa peer pull (C):** `cashbox_upsert` / `cashbox_deleted` → `pos_cashbox` gölge + `GET /pos/cashbox` soft-merge (`quickly:cashbox-soft-merged`). Open-check poll ile aynı ~5 sn. KDS: ürün `status` open-check gölgesinde; soft-patch masa **WILL_READY(3)**. E2E plane C: `e2e-cross-device-sync.mjs`.
>
> **2026.07.23 — Canlı peer sync (A+B):** Masalar ekranı soft-upsert sonrası **NgZone** ile yenilenir (`quickly:open-checks-soft-merged` + Pouch change); kilit/açmadan görünür. Open-check poll **~5 sn**. Rapor/Hesaplar düzenlemesi → `sale_updated` + `pos_closed_check` gölge + `GET /pos/closed-checks` soft-merge (`quickly:closed-checks-soft-merged`). SSE yok (polling). E2E: `node hq-api/scripts/e2e-cross-device-sync.mjs`.
>
> **2026.07.23 — Dual-offline aynı masa:** Kardeş açık check varken ödeme masayı FREE etmez; mPOS çoklu adisyon seçici + `N adisyon` rozet; soft-merge `quickly:table-check-collision`; online `POST /pos/table-claims` (409); outbox 500 drop → `quickly:outbox-dropped` toast. Satır birleştirme **yok**. mPOS 20260723az.
>
> **2026.07.23 — Ölçek soft-refresh:** Hesap dashboard soft-refresh artık yalnızca `day-status` + `open-checks` (2 API), ~28s / idle 60s / gizli sekme 120s; değişmeyen fingerprint atlanır. POS open-checks soft-upsert **~8s**; closed-checks + cashbox **her 3. tur**; 429’da `Retry-After` veya üssel backoff. Ortak limiter `portal-pos-open-checks` **540/5dk** (file veya REDIS_URL) + kiracı `open_checks/by_tenant_open` view.
>
> **2026.07.22.27 — Sync harden:** Outbox pending replace → `next_retry_at` temizle; `flushAndWaitAck`/`force` backoff’u aşar; `check_status`/`product.status` 0 artık `||` ile bozulmaz; boş PASSIVE stub shadow’a gitmez; same-table adopt önce remote put sonra stub sil; Hesap açık masalar soft-refresh (sonra 2026-07-23’te hafifletildi). web 20260722y / desktop 2026.07.22.27.
>
> **2026.07.22.26 — Web↔POS senkron yığını:** Shadow-gated ACK (`check_*` → shadow fail = `ingested: false`); duplicate yolu shadow re-apply; `pullAgain`; stable outbox `chkup_<checkId>` + pending payload replace; same-table empty/PASSIVE adopt; `local_tables` occupancy preserve; pack tables always free; selling-screen NgZone + soft-merge/catalog listeners; `isLivePosSession` = durable JWT + app-root. **Gecikme sözleşmesi:** tipik **1–4 sn** (flush + pullOnce / visibility); en kötü ~**12 sn** poll. web 20260722x / desktop 2026.07.22.26.
>
> **2026.07.22.25 — Silent catalog + open-check UX:** Canlı vardiyada katalog soft-merge → overlay/reload/`oturum doğrulanıyor` **yok** (chip: “Menü güncellendi”). Gate quiet resume. Open-check soft-upsert ~12s + `local_tables.status` soft-patch + ürün satırları; `check_upsert` sonrası pullOnce. web 20260722w / desktop 2026.07.22.25.
>
> **QA doğrulama (host):** `bash hq-api/scripts/e2e-pos-az-qa.sh` — Bug1 PASS (overlay yok) · Bug2 PASS (dual web POS soft-upsert ≤15s, ürün satırları + masa rengi). EXE tıklaması Windows VM checklist’te.

> **2026.07.22.24 — Opus kalite:** Güncelle → `desktop_exe_url` önce + `quicklyDesktop.openExternal`; Lisans asla çıplak `-` (TenantId varken Portal/Bağlı); EOD 10s timeout + yerel Pouch soft-fill; ingest ACK yalnızca `ingested` (duplicate re-ingest kapısı). web 20260722v / desktop 2026.07.22.24.

> **2026.07.22.23 — EOD day reports:** Web IndexedDB gün yedeği + bulut fallback; yanlış “senkron bekleyin” kilidi kaldırıldı. web 20260722u / desktop 2026.07.22.23.

> **2026.07.22.22 — Download nav + Lisans label:** Güncelle → Desktop zip (`openExternal`); portal/istasyon kurulumlarda Lisans alanı boş tire yerine “Bağlı (kalıcı link)”.

> **2026.07.22.21 — Sync hardening:** Ingest ACK yalnızca `ingested` sonrası; duplicate yolu re-ingest. Outbox backoff + JWT station recover. Ordered sync → katalog + day-status + soft-upsert. `day_closed` + flushAndWaitAck. Kilit heartbeat + pagehide release. Hesap dashboard soft-refresh.

> **2026.07.22.20 — Soft-upsert + kilit UI:** Diğer POS cihazları açık masa gölgesini LWW ile `local_checks`’e soft-merge eder (wipe yok). Adisyon açılışında `QuicklyCheckLock.claim`; 409’da Türkçe modal (kilit sahibi / Tekrar Dene / Geri Dön).

> **2026.07.22.19 — Üç düzlem senkron:** Online + JWT yok → sessiz `grace_no_session` **yok** (Hesap’a yönlendir). Gün durumu çift yön (`pos_day_status` + `day-sync.js`). Açık masa gölgesi Hesap’ta salt okunur. Katalog soft-merge; açık check pack’ten silinmez.

---

## 0. Üç düzlem (kilit model)

| Düzlem | SoT | Yön | Not |
|--------|-----|-----|-----|
| **A Katalog** | Hesap (packs) | cloud → tüm POS | Soft-merge; `NEVER_TOUCH` DB’ler pack’te yok |
| **B İş günü** | `pos_day_status` | çift yön | POS push `day_*`; POS pull soft-apply DateSettings (açık check varsa onay) |
| **C Satış / masa** | cihaz açık check; bulut gölge + kapalı satış | desktop/web ↔ diğer POS (soft) | `check_upsert` / `sale_closed`; soft-upsert LWW + advisory lock |

**Kural:** Açık adisyonları katalog wipe pack’lerine **asla** koyma.

---

## 1. Kaynak-of-truth (SoT) bölümleri

| Bölüm | Sahip (tek yazar) | Örnekler | Karşı taraf ne yapar? |
|-------|-------------------|----------|------------------------|
| **Katalog / bulut** | Hesap (hq-api packs) | ürün, kategori, fiyat, masa/kat, müşteri kartı, bulut ayarları, lisans durumu | POS yalnızca `remote_version > applied_version` ise uygular; katalog düzenlemesi POS’tan push edilmez |
| **Operasyonel / cihaz** | O cihaz (`device_id`) | açık adisyon (`local_checks`), kasa oturumu, `DateSettings`, gün sonu yerel, yazıcı/PIN yerel | Bulut pull **asla** silmez/üzerine yazmaz (`NEVER_TOUCH`); yabancı check soft-upsert ayrı kanal |
| **Append-only olaylar** | Cihaz → bulut | tamamlanan satış özeti, ödeme, EOD özeti, açık masa gölgesi | Sunucu `idempotency_key` ile tekilleştirir |

---

## 2. Mevcut kod ile hizalama

- `pos/catalog-bridge.js`: `CLOUD_OWNED` / `NEVER_TOUCH` / `MENU_SOFT_MERGE`
- `pos/day-sync.js`: Plane B poll (~30s) + soft DateSettings + `quickly:ordered-sync`
- `pos/check-device.js`: soft-upsert (~12s) + lock claim/heartbeat/release + table status soft-patch + `QuicklyCheckLock.showConflict`
- `pos/sync-outbox.js`: backoff, JWT recover, debounced flush, planes B+C after ordered sync
- `hq-api`: `POST /pos/events` (ack yalnızca ingest sonrası), `GET /pos/day-status`, `GET /pos/open-checks`, `GET /pos/closed-checks`, `GET /pos/cashbox`, `POST/GET /pos/check-locks`, `POST /pos/table-claims`

---

## 3. Çakışma önleme kuralları

1. **Katalog:** Monoton sürüm; soft-merge; DateSettings pack’ten çıkarılır.
2. **Operasyon:** Açık check’ler öncelikle cihaza aittir (`origin_device_id`). Soft-upsert yalnızca **yabancı** gölgeleri LWW ile yazar; kendi check’i yapısal olarak üzerine yazılmaz (mutfak `product.status` peer patch’i hariç). Yabancı origin **structural re-push edilmez**; KDS «Hazır» için `kitchen_ready_patch` istisnası var.
3. **Advisory lock:** Adisyon ekranı açılınca `claim`; ~90s heartbeat; `pagehide`/`beforeunload` → `release`. Başka cihaz tutuyorsa **409** → modal. Dual-edit pixel-perfect clone **kapsam dışı**.
4. **Yeniden bağlanınca sıra:** (1) outbox push (2) katalog pull (3) soft-merge (4) day-status soft-apply (5) open-checks soft-upsert
5. **Online + JWT yok:** Grace **yok** — istasyon linki / Hesap zorunlu. Flush 401 → cached station re-consume.
6. **Gün başı/sonu:** Reload öncesi `flushAndWaitAck` (`day_started` / `day_closed` + EOD).
7. **Dual-offline aynı masa:** İki cihaz offline aynı `table_id`’ye ayrı `check_id` açabilir. **Satır birleştirme yok.** UI: çoklu adisyon seçici; ödeme sonrası kardeş check varken masa OCCUPIED kalır. Online: `table_claim` ikinci cihazı 409 ile uyarır (yine de “yine de aç” ile devam edilebilir). Soft-merge `collisions[]` → `quickly:table-check-collision`.
8. **Outbox tavanı:** `MAX_OUTBOX=500` aşılınca en eski olaylar düşer → `quickly:outbox-dropped` (ciro riski uyarısı).

---

## 3.1 Diğer POS soft-upsert (açık masa 2. aşama)

| Adım | Davranış |
|------|----------|
| Push | Yerel açık check → sanitize özet (`products` adı/fiyat + **mutfak ürün notu** trim≤120; owner/telefon yok) → `check_upsert` |
| Shadow | hq-api `pos_open_check` (LWW `client_timestamp`) |
| Push uuid | Stable `chkup_<checkId>` — pending outbox **payload replace** (Date.now anahtar yok) |
| Pull | Her ~**8s** `GET /pos/open-checks` + locks; `GET /pos/closed-checks` + `GET /pos/cashbox` her **3. tur** (~24s); 429 → backoff skip (+ online / ordered-sync / catalog-applied / visibility); `pullAgain` if busy |
| Latency | Tipik **1–4 sn** (enqueue→flush→shadow→karşı POS `pullOnce`); worst-case ~**8 sn** open poll |
| Merge | `origin_device_id !== mine` ve bu cihaz lock tutmuyorsa → `local_checks` put (`soft_upsert_remote: true`) + `local_tables.status=2` soft-patch |
| UI | Masalar (`store`) + selling-screen: `quickly:open-checks-soft-merged` → **NgZone** `fillData` (kilit/aç gerekmez). `window.QuicklyNgZone` nudge |
| Adopt | Aynı `table_id`: yabancı açık + yerel empty/PASSIVE stub → stub sil, remote `_id` bağla |
| Skip | Kendi **dolu** check’i; lock bu cihazdaysa; LWW yerel daha yeniyse; yabancı origin re-push yok |
| Close | Bulutta açık değilse yabancı soft stub `remove` (yerel `check_closed` **yayılmaz**) + masa `status=1` |
| Pack | Soft-upsert **katalog wipe pack’lerinde yok**; masa pack `status=1` (free); merge yerel occupancy korur; canlı apply reload yapmaz |
| ACK | `check_upsert|snapshot|closed`: shadow başarısız → `ingested: false` (ACK yok); duplicate → shadow re-apply |
| Rapor edit | `sale_updated` → `pos_closed_check` LWW; peer soft-merge → `quickly:closed-checks-soft-merged` |

DevTools: `QuicklyCheckLock.pullOnce()` · `QuicklyCheckLock.claim(checkId)`

---

## 3.15 Sync düzlem matrisi (multi-device)

| Düzlem | Mekanizma | Gap / not | Öncelik |
|--------|-----------|-----------|---------|
| Açık masa / claim | `check_upsert` + soft-upsert + `table_claims` | UI NgZone fix **20260723bk**; poll ~5s | P0 DONE |
| Canlı satır (add/void/iskonto) | aynı open-check shadow LWW | Pixel-perfect dual-edit **kapsam dışı**; kilit 409 | P1 bilinçli |
| Kapat / ödeme / split | `sale_closed` + `check_closed` + payment | Peer açık gölge kalkar; kapalı shadow yeni | P0 kısmen |
| Gün başı/sonu | `day_*` + day-sync poll ~30s | OK | DONE |
| Katalog | packs soft-merge | OK (silent) | DONE |
| Personel / PIN / roller | catalog staff + bond | Offline puantaj kuyruğu P3 | P1/P3 |
| Rapor eski hesap edit | `sale_updated` + closed soft-merge | **20260723bk**; Store `weekly[]` peer rebuild **20260723br** (`quickly:reports-weekly-rebuilt`) | P1 DONE |
| Kasa hareketleri | `cashbox_upsert`/`deleted` + `GET /pos/cashbox` soft-merge | **20260723bq**; yerel sahip SoT; yabancı soft stub | P1 DONE |
| KDS biletleri | open-check ürün `status` 1/4/2 + `kitchen_ready_patch` + WILL_READY | Üç sütun + Angular confirm fix **20260724cg** | P1 DONE |
| SSE | — | Bilinçli yok; poll 5s | P3 ertelendi |
| Ayarlar / yazıcı / işletme | AppSettings local + catalog | Yazıcı cihaz-local SoT | OK |
| SSE/WebSocket | **yok** — poll + visibility + ordered-sync | Gerçek zaman için P2 SSE opsiyonel | P2 |

> Visibility `ordered-sync` Electron’da sekme blur olmadan tetiklenmez; bu yüzden open/closed **interval poll** şart. Kilit/aç → `pos-ready` / `loadAppData` eski “workaround”dı.

---

## 3.2 Kilit 409 modalı

- Angular `selling-screen` → `claimActiveCheckLock()` (`QuicklyCheckLock.claim`)
- 409 / `lock_held` → navy `#2b3e50` + accent `#e62531` modal: kilit sahibi, **Tekrar Dene**, **Geri Dön**
- `ngOnDestroy` + `pagehide` → `release`; heartbeat ~90s
- Kaynak: `quickly-desktop-master/.../selling-screen.component.ts` (webpack → `pos/main.bundle.js`)

---

## 4. Outbox kayıt şekli

```json
{
  "idempotency_key": "dev_xxx:evt_yyy:42",
  "device_id": "dev_xxx",
  "kind": "sale_closed|sale_updated|payment|eod_summary|day_started|day_closed|check_upsert|check_closed|cashbox_upsert|cashbox_deleted",
  "payload": { }
}
```

Pending aynı `local_uuid`+`kind` → **payload replace** (aynı idempotency_key). HTTP hata → exponential backoff (`next_retry_at`).

---

## 5. Yeniden bağlanma protokolü (istemci)

`pos/sync-outbox.js` → `runOrderedSync()` + health chip (oturum · kuyruk · son push/pull).

Sıra: flush → catalog pull → `QuicklyDaySync.pollOnce` + `QuicklyCheckLock.pullOnce`.

`pos/sync-hooks.js`: last-N backfill (80); `DateSettings` → `day_closed`; `markSeen` yalnızca başarılı enqueue sonrası.

---

## 6. Offline masaüstü

Electron paketi POS UI’yi yerel dosyadan sunar. `/api/*` buluta proxy.  
İlk kurulum: lisans / istasyon linki / ilk katalog için internet.

### 6.1 Oturum / UX

| Durum | Beklenen |
|-------|----------|
| Proxy 502 / timeout | Oturum silinmez; grace ile POS |
| Gerçek HTTP 401 | Token temizlenir **veya** station re-consume ile yenilenir |
| Online + JWT yok + istasyon yok | Hesap’a yönlendir (senkron ölü satış modu yok) |
| Yeniden online | outbox → katalog → day-sync → soft-upsert |

---

## 7. VM / manuel doğrulama checklist

Tam adımlı Windows VM listesi (pass/fail): [`docs/WINDOWS-VM-SMOKE.md`](./WINDOWS-VM-SMOKE.md) · canlı kopya: `/downloads/WINDOWS-VM-SMOKE.md` (desktop **2026.07.22.25** / web **20260722w**).

1. Desktop online + istasyon: masa kapat / gün başı / gün sonu → Hesap `#/sync-events` + gün durumu
2. Web gün başı/sonu → desktop DateSettings / UI aynı durum (`day-sync` onay diyaloğu)
3. Online JWT yok → sessiz satış modu **değil**; Hesap yönlendirmesi
4. Katalog soft-merge; açık check pack ile silinmez
5. Hesap özet: **Açık masalar** paneli (salt okunur gölge; ~28s soft-refresh)
6. **İki POS:** A’da masa aç → B’de ~12–15s içinde aynı masa soft-upsert (tutar/kalem + yeşil masa); B düzenlerken A’da 409 modal
7. **Hesap silme:** ürün sil → POS kesintisiz; chip “Menü güncellendi”; `oturum doğrulanıyor` overlay **yok**
7. DevTools: `QuicklySync.sessionTokenPresent()` · `flushAndWaitAck()` · `QuicklyDaySync.pollOnce()` · `QuicklyCheckLock.pullOnce()` · `QuicklySync.tryRecoverSession()`

---

## 8. Faz notları

- [x] `origin_device_id` + soft check-locks
- [x] day_status çift yön + open-check shadow
- [x] Angular kilit 409 modalı
- [x] Diğer POS soft-upsert (2. aşama)
- [x] Sync hardening (ingest/outbox/day_closed/heartbeat/Hesap refresh)
- Authenticode / MSI
- Pixel-perfect dual-edit realtime clone — **bilinçli kapsam dışı**
