agent-enderun 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/mimari.md DELETED
@@ -1,926 +0,0 @@
1
- # KENTİM — Çok Belediyeli (Multi-Tenant SaaS) Mimari Belgesi (MVP Sürümü)
2
-
3
- Bu belge, KENTİM sisteminin tek bir belediye sınırlarının ötesinde, Türkiye genelindeki **birden fazla bağımsız belediyeye (Multi-Tenant SaaS)** nasıl ölçeklendiğini, veri izolasyonu güvenlik katmanlarını ve paylaşılan merkezi ağ geçidi yapısını tanımlar.
4
-
5
- ---
6
-
7
- ## 1. Çok Belediyeli (Multi-Tenant) Sistem Mimarisi
8
-
9
- KENTİM, vatandaşların tek bir mobil uygulama veya web arayüzü üzerinden coğrafi konumlarına göre doğru belediyeyle etkileşime girdiği; belediyelerin ise birbirlerinin verilerini kesinlikle göremediği **izole edilmiş çok kiracılı (multi-tenant) bir SaaS** mimarisine sahiptir.
10
-
11
- ### Çok Belediyeli Sistem Akış Diyagramı (SaaS)
12
-
13
- Aşağıdaki diyagramda görüldüğü üzere, vatandaş katmanı paylaşımlı bir ağ geçidi (Central Gateway) üzerinden gelirken, belediye verileri, personelleri ve iş akışları `tenant_id` bazında PostGIS ve RLS katmanlarıyla tamamen izole edilmiştir.
14
-
15
- ```mermaid
16
- graph TD
17
- subgraph Vatandaş Katmanı (Paylaşılan Mobil / Web Arayüzü)
18
- Citizen[Vatandaş Cihazı] -->|Rapor Oluşturma & Takip| CentralGateway[Central SaaS Gateway API]
19
- end
20
-
21
- subgraph Central SaaS Katmanı (central.kentim.com.tr)
22
- CentralGateway -->|1. Geofencing ile Belediye Tespiti| TenantRouter[Tenant Router / PostGIS Engine]
23
- CentralAdmin[Central Admin] -->|Abonelik & Global Ayarlar| CentralDB[(Central SaaS DB)]
24
- TenantRouter -->|Lisans Kontrolü| CentralDB
25
- end
26
-
27
- subgraph Belediye A İzolasyon Sınırı (tenant_id: muni_a)
28
- TenantRouter -->|Yönlendirme| MuniA_Mod[Muni A Moderatörü]
29
- MuniA_Mod -->|Müdürlük Havuzu| MuniA_Mgr[Muni A Departman Müdürü]
30
- MuniA_Mgr -->|Şef Havuzu| MuniA_Chief[Muni A Ekip Şefi]
31
- MuniA_Chief -->|Görev Atama| MuniA_Worker[Muni A Saha Personeli]
32
- MuniA_Worker -.->|Foto & GPS Kanıtları| MuniA_DB[(Belediye A DB / Schema)]
33
- MuniA_Admin[Muni A Admin] -->|Kriz Masası & Ayarlar| MuniA_DB
34
- end
35
-
36
- subgraph Belediye B İzolasyon Sınırı (tenant_id: muni_b)
37
- TenantRouter -->|Yönlendirme| MuniB_Mod[Muni B Moderatörü]
38
- MuniB_Mod -->|Müdürlük Havuzu| MuniB_Mgr[Muni B Departman Müdürü]
39
- MuniB_Mgr -->|Şef Havuzu| MuniB_Chief[Muni B Ekip Şefi]
40
- MuniB_Chief -->|Görev Atama| MuniB_Worker[Muni B Saha Personeli]
41
- MuniB_Worker -.->|Foto & GPS Kanıtları| MuniB_DB[(Belediye B DB / Schema)]
42
- MuniB_Admin[Muni B Admin] -->|Kriz Masası & Ayarlar| MuniB_DB
43
- end
44
- ```
45
-
46
- ---
47
-
48
- ## 2. Çoklu Belediye Veri İzolasyon Modeli
49
-
50
- Çok belediyeli yapının veritabanı seviyesindeki güvenliği ve izolasyonu üç ana katmanda kurgulanmıştır:
51
-
52
- ### A. Central SaaS Veritabanı (Paylaşılan Katman)
53
- Tüm sistemin ortak yönetim verilerini tutar. Belediyeler bu veritabanına doğrudan yazamaz veya erişemez:
54
- * Belediye kayıtları (Tenant Registry: `muni_a`, `muni_b` listesi)
55
- * Lisanslama ve Yıllık Abonelik Takibi (Başlangıç, bitiş tarihleri, ödeme durumları)
56
- * **Global Rapor Kategorileri & Seed Yapısı (`global_categories` tablosu)**:
57
- Sisteme dahil olan tüm belediyelerin varsayılan olarak devraldığı, central admin tarafından yönetilen global şikayet kategorileridir.
58
- - **`global_categories` SQL Tablo Şeması:**
59
- ```sql
60
- CREATE TABLE global_categories (
61
- id SERIAL PRIMARY KEY,
62
- name VARCHAR(100) NOT NULL UNIQUE,
63
- risk_score INTEGER NOT NULL CHECK (risk_score BETWEEN 1 AND 10),
64
- default_public_visible BOOLEAN DEFAULT TRUE,
65
- created_at TIMESTAMPTZ DEFAULT NOW()
66
- );
67
- ```
68
- - **Otomatik Kurulum & Kategori Seed'i (`global_categories_seed.sql`)**:
69
- Sistem ayağa kaldırılırken çalışan bu seed dosyası, Türkiye'deki belediyecilik standartlarına uygun temel kategorileri otomatik olarak enjekte eder:
70
- ```sql
71
- INSERT INTO global_categories (name, risk_score, default_public_visible) VALUES
72
- ('Yol Hasarı / Çukur', 5, TRUE),
73
- ('Çöp / Temizlik Şikayeti', 3, TRUE),
74
- ('Su ve Kanalizasyon Arızası', 8, TRUE),
75
- ('Park ve Bahçe Bakımı', 2, TRUE),
76
- ('Sokak Aydınlatması Arızası', 4, TRUE),
77
- ('Zabıta Şikayeti / İşgal', 6, TRUE);
78
- ```
79
-
80
- * **Merkezi Lokasyon Yönetimi & Hiyerarşisi (`central_locations` tablosu)**: Türkiye geneli il ve ilçe verileri central seviyede bir sözlük olarak tutulur. Belediyelerin kayıt esnasında seçtiği il/ilçe alanları doğrudan bu tablodan beslenir.
81
- - **`central_locations` SQL Tablo Şeması:**
82
- ```sql
83
- CREATE TABLE central_locations (
84
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
85
- name VARCHAR(100) NOT NULL,
86
- parent_id UUID NULL REFERENCES central_locations(id) ON DELETE CASCADE,
87
- type VARCHAR(20) NOT NULL CHECK (type IN ('province', 'district')),
88
- slug VARCHAR(120) NOT NULL,
89
- created_at TIMESTAMPTZ DEFAULT NOW(),
90
- CONSTRAINT unique_name_parent UNIQUE (name, parent_id)
91
- );
92
- CREATE INDEX idx_central_locations_parent ON central_locations(parent_id);
93
- CREATE INDEX idx_central_locations_type ON central_locations(type);
94
- CREATE INDEX idx_central_locations_slug ON central_locations(slug);
95
- ```
96
- - **Türkiye Coğrafi Seed Yapısı (`turkey_locations_seed.sql`)**:
97
- Sistemin kurulduğu ilk anda çalıştırılan bu SQL göç (migration) betiği, Türkiye'deki **tam olarak 81 ili** (`type = 'province'`) ve bu illere hiyerarşik olarak bağlı **973 ilçeyi** (`type = 'district'`) `central_locations` tablosuna eksiksiz enjekte eder. Örneğin:
98
- ```sql
99
- -- Örnek İl Enjeksiyonu
100
- INSERT INTO central_locations (id, name, parent_id, type, slug) VALUES ('a2e564ad-43ab-41c1-90a1-24c568ad9cf1', 'İstanbul', NULL, 'province', 'istanbul');
101
- -- Örnek İlçe Enjeksiyonu (İstanbul'a Bağlı)
102
- INSERT INTO central_locations (name, parent_id, type, slug) VALUES ('Kadıköy', 'a2e564ad-43ab-41c1-90a1-24c568ad9cf1', 'district', 'kadikoy');
103
- ```
104
- * **Merkezi Denetim Logları (`central_audit_logs` tablosu)**: Central SaaS düzeyinde sistem yöneticilerinin gerçekleştirdiği tüm kritik eylemleri, lisans güncellemelerini ve belediye kayıtlarını değişen verilerin geçmiş halleriyle birlikte takip eden global işlem logları tablosudur.
105
- - **`central_audit_logs` SQL Tablo Şeması:**
106
- ```sql
107
- CREATE TABLE central_audit_logs (
108
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
109
- central_user_id UUID NOT NULL, -- İşlemi gerçekleştiren Central Admin personeli
110
- municipality_id UUID NULL, -- Eğer bir belediye etkilenmişse (örn: lisans askıya alma) referans
111
- event_type VARCHAR(50) NOT NULL, -- örn: 'MUNICIPALITY_REGISTERED', 'LICENSE_SUSPENDED', 'GLOBAL_CONFIG_UPDATED'
112
- ip_address VARCHAR(45) NOT NULL, -- IPv4 or IPv6 support
113
- user_agent TEXT NULL,
114
- payload_old JSONB NULL, -- Güncelleme öncesi ham veri satırı (veri eklemede NULL)
115
- payload_new JSONB NULL, -- Güncelleme sonrası ham veri satırı (veri silmede NULL)
116
- created_at TIMESTAMPTZ DEFAULT NOW()
117
- );
118
- CREATE INDEX idx_central_audit_logs_event ON central_audit_logs(event_type);
119
- CREATE INDEX idx_central_audit_logs_muni ON central_audit_logs(municipality_id);
120
- CREATE INDEX idx_central_audit_logs_created ON central_audit_logs(created_at);
121
- ```
122
- * **Merkezi Statik İçerik Yönetimi (`central_static_contents` tablosu)**: SaaS paneli üzerinden "Hakkımızda", "KVKK", "Gizlilik" gibi metinlerin başlık ve içeriklerini yönetmek için kullanılır.
123
-
124
- **Public Web Yol Haritası için Genişletme Önerisi (V1):**
125
- - `public_web_roadmap_mvp` → MVP sürümü yol haritası içeriği
126
- - `public_web_roadmap_v1` → V1 sürümü yol haritası içeriği
127
-
128
- **Migration Önerisi (Mevcut tabloyu bozmadan):**
129
-
130
- ```sql
131
- -- MVP Yol Haritası
132
- INSERT INTO central_static_contents (key, title, content, updated_at)
133
- VALUES (
134
- 'public_web_roadmap_mvp',
135
- 'MVP Sürümünde Neler Var?',
136
- '# MVP Sürümünde Neler Var?\n\n- Temel raporlama ve takip\n- +1 desteği\n- Temel katılım araçları\n- ...',
137
- NOW()
138
- )
139
- ON CONFLICT (key) DO NOTHING;
140
-
141
- -- V1 Yol Haritası
142
- INSERT INTO central_static_contents (key, title, content, updated_at)
143
- VALUES (
144
- 'public_web_roadmap_v1',
145
- 'V1 Sürümünde Gelen Özellikler',
146
- '# V1 Sürümünde Gelen Özellikler\n\n- Gelişmiş harita deneyimi\n- AI destekli öneriler\n- ...',
147
- NOW()
148
- )
149
- ON CONFLICT (key) DO NOTHING;
150
- ```
151
-
152
- **Not:** Schema değişikliği gerekmez. Sadece yeni satır INSERT yeterlidir. Central Admin panelinden güncellenen içerikler bu key’ler üzerinden tutulur.
153
- - **`central_static_contents` SQL Tablo Şeması:**
154
- ```sql
155
- CREATE TABLE central_static_contents (
156
- key VARCHAR(50) PRIMARY KEY,
157
- title VARCHAR(200) NOT NULL,
158
- content TEXT,
159
- updated_at TIMESTAMPTZ DEFAULT NOW()
160
- );
161
- CREATE INDEX idx_central_static_contents_updated ON central_static_contents(updated_at);
162
- ```
163
- * Global Vatandaş Kayıtları (`citizen` tablosu): Vatandaş kimlik, iletişim ve asenkron doğrulanmış T.C. kimlik bilgileri central düzeyde saklanır.
164
-
165
- ### B. Belediye Veritabanları / Şemaları (İzole Katman)
166
- Her belediye kendi verilerini izole bir şemada (`schema_muni_a`) veya bağımsız veritabanı sunucularında tutar.
167
- * **İş Emirleri & Vatandaş Raporları**: Sadece ilgili belediye şemasına ait UUIDv4 kayıtları. `work_orders` tablosunda public harita alanlarının yanı sıra, müdür bypass atamasını ve geofence bypass durumunu yöneten `is_manager_bypassed` (BOOLEAN DEFAULT FALSE) ve `geofence_bypass_requested` (BOOLEAN DEFAULT FALSE) alanları; kronik sorunları birbirine bağlayan `parent_reopened_work_order_id UUID NULL (ref: work_orders.id ON DELETE SET NULL)` alanı; ve iş emri oluşturulduğu andaki PostGIS belediye sınır sürümünü işaret eden `boundary_version_id UUID NULL (ref: municipal_boundary_versions.id)` alanı mevcuttur. Sınır geometrisinin her satırda tekrarlanması engellenerek depolama alanı şişmesi (bloating) önlenmiştir. RLS ve veri güvenliği politikaları bu alanları da kapsar.
168
- * **Vatandaş Referansları**: Belediyeler vatandaş verilerine doğrudan erişemez; iş emirlerinde ve oylarda sadece Central SaaS DB'deki vatandaş kaydına ait `citizen_id` (UUID) referans olarak tutulur. GDPR silme taleplerinde sadece ilişkiler maskelenir.
169
-
170
- **Vatandaş Harita Deneyimi Notu:** Ana sayfa (`/`) doğrudan public harita olarak açılır. İlk açılış **Türkiye geneli** (düşük zoom) seviyesinde başlar. Kullanıcı zoom yaptıkça ve haritayı kaydırdıkça sistem otomatik olarak il → ilçe → mahalle → sokak seviyesine iner.
171
-
172
- **Statik İçerikler:** Hakkımızda, SSS, Gizlilik Politikası, KVKK, Kullanım Koşulları ve İletişim gibi yasal/bilgilendirici içerikler artık ayrı tam sayfa olarak değil, ana sayfadaki public harita üzerinden modal olarak açılır.
173
- * **Destek Oyları (support_votes) & Sayaç (support_count)**: Çözülmemiş sorunlara verilen oyları tutan `support_votes(id, work_order_id, citizen_id, created_at)` tablosu ile `(work_order_id, citizen_id)` üzerinde UNIQUE constraint uygulanır.
174
- **Ölçeklenebilirlik & Row-Locking Önleme Tasarımı:** Popüler veya kriz anındaki şikayetlerde oluşabilecek yoğun "+1" isteklerinin `work_orders` tablosunda row-lock (satır kilitleme) darboğazı yaratmasını engellemek için, oylar doğrudan `support_votes` tablosuna yazılır (INSERT işlemi kilit oluşturmaz). `work_orders.support_count` (INTEGER DEFAULT 0) sayacı ise gerçek zamanlı DB trigger yerine, PostgreSQL LISTEN/NOTIFY tabanlı bir sayaç/kuyruk üzerinden debounced asenkron worker ile (örn. 5 dakikalık aralıklarla toplu / bulk update şeklinde) PostgreSQL'e yansıtılarak ana operasyonel tabloların kilitlenmesi önlenir.
175
- * **Personel & Roller**: Belediyenin kendi bünyesindeki müdür, şef ve saha çalışanları.
176
- * **Coğrafi Sınurlar ve Sürüm Kontrolü (Boundaries & Versions)**: Belediyenin PostGIS sınır poligonları, depolama şişmesini önlemek amacıyla `municipal_boundary_versions` tablosunda sürüm kontrolüyle saklanır.
177
- - **municipal_boundary_versions Tablo Yapısı:**
178
- - `id UUID PRIMARY KEY DEFAULT gen_random_uuid()`
179
- - `municipality_id UUID (ref: municipalities.id)`
180
- - `boundaries GEOMETRY(MultiPolygon, 4326) NOT NULL` (PostGIS sınır poligonu)
181
- - `active_from TIMESTAMPTZ NOT NULL`
182
- - `active_to TIMESTAMPTZ NULL` (Mevcut sürüm için NULL)
183
- - `created_at TIMESTAMPTZ DEFAULT NOW()`
184
- * **Belediye Düzeyi Denetim Logları (`audit_logs` tablosu)**: Belediyenin kendi süreç içi denetim kayıtları. İş emirleri güncellemeleri, durum değişiklikleri, personel atamaları ve geofence bypass onayları gibi tüm kritik veri modifikasyonları, işlemin öncesi ve sonrası raw halini (JSONB) içerecek şekilde kaydedilir. KVKK silme talebi kapsamında yapılacak asenkron regex maskeleme sorgularında tam tablo taramasını (Full Table Scan) önlemek amacıyla, `audit_logs` tablosunda indeksli bir `citizen_id` (UUID NULL) kolonu bulundurulması zorunludur.
185
- - **`audit_logs` SQL Tablo Şeması:**
186
- ```sql
187
- CREATE TABLE audit_logs (
188
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
189
- tenant_id VARCHAR(50) NOT NULL,
190
- user_id UUID NOT NULL, -- İşlemi gerçekleştiren belediye personeli (müdür/şef/saha)
191
- citizen_id UUID NULL, -- İşleme konu olan vatandaş (KVKK maskeleme performansı için İNDEKSLİ)
192
- work_order_id UUID NULL, -- İşleme konu olan iş emri UUID referansı
193
- event_type VARCHAR(50) NOT NULL, -- örn: 'WORK_ORDER_STATUS_CHANGED', 'GEOFENCE_BYPASS_APPROVED', 'DISPUTE_RESOLVED'
194
- ip_address VARCHAR(45) NOT NULL,
195
- user_agent TEXT NULL,
196
- payload_old JSONB NULL, -- Güncelleme öncesi ham veri satırı (veri eklemede NULL)
197
- payload_new JSONB NULL, -- Güncelleme sonrası ham veri satırı (veri silmede NULL)
198
- created_at TIMESTAMPTZ DEFAULT NOW()
199
- );
200
- CREATE INDEX idx_audit_logs_tenant ON audit_logs(tenant_id);
201
- CREATE INDEX idx_audit_logs_citizen_id ON audit_logs(citizen_id) WHERE citizen_id IS NOT NULL; -- KVKK performansı için kısmi indeks
202
- CREATE INDEX idx_audit_logs_work_order ON audit_logs(work_order_id);
203
- CREATE INDEX idx_audit_logs_event_created ON audit_logs(event_type, created_at);
204
- ```
205
- * **Uyuşmazlık Kayıtları (dispute_logs)**: Çevrimdışı senkronizasyon çakışmalarını izler. Şef onay ekranında bütçe/hak ediş kayıplarını ve mükerrer ödemeleri engellemek için veritabanı seviyesinde çalışan "Çift Hak Ediş Uyarısı (Double Payment Alert)" tetikleyici (trigger) mekanizması entegre edilmiştir.
206
- * **IoT Cihaz Tanımları & Alan Eşleştirmeleri**: Belediyenin kendi envanterine kayıtlı IoT aygıtları, API anahtarı eşleşmeleri ve admin tarafından tanımlanmış "Custom Payload Mapping" (GUI/JS Parser) kuralları. **V8 Sandbox Kısıtları & Pre-Filtering:** Güvenlik ve kaynak tüketimini kontrol altında tutmak için parser'a girmeden önce ham telemetri payload boyutu **maksimum 10KB** ile sınırlandırılır. Izole parser scriptleri, CPU tüketimini sınırlandırmak için **maksimum 50ms CPU zamanı** ve **maksimum 16MB RAM** ile sınırlandırılmıştır. Harici network veya disk işlemleri kesinlikle engellenmiştir.
207
- * **Telemetri Logları (Telemetry Logs)**: Sensörlerden gelen ham ve parse edilmiş geçmiş veriler (yalnızca ilgili belediyenin şemasında saklanır).
208
-
209
- ### C. PostgreSQL Satır Bazlı Güvenlik (Row-Level Security - RLS)
210
- Belediyeler aynı fiziksel veritabanı kümesini paylaşsa dahi, RLS politikaları sayesinde veri sızıntısı imkansız hale getirilir.
211
-
212
- #### Örnek RLS Politikası (SQL Örneği):
213
- ```sql
214
- -- İş emirleri tablosunda Row-Level Security aktifleştirme
215
- ALTER TABLE work_orders ENABLE ROW LEVEL SECURITY;
216
-
217
- -- Sadece kullanıcının JWT token'ından gelen tenant_id ile eşleşen satırları okuma politikası
218
- CREATE POLICY tenant_isolation_policy ON work_orders
219
- USING (tenant_id = current_setting('app.current_tenant_id'));
220
-
221
- -- Anonim/Public Harita Okuma RLS Politikası
222
- CREATE POLICY public_map_read_policy ON work_orders
223
- FOR SELECT
224
- USING (is_public_visible = TRUE AND status NOT IN ('pending', 'rejected'));
225
- ```
226
-
227
- #### Halka Açık Görünüm Katmanı (public_work_orders_view)
228
- Anonim `GET /v1/public/reports` istekleri için doğrudan tablolara erişim yerine `public_work_orders_view` üzerinden veri sunulur. Bu view:
229
- 1. Kişisel verileri (`citizen_id`, ad, soyad, telefon, e-posta) sunucu seviyesinde filtreler (dışlar).
230
- 2. Tam GPS koordinatları yerine ±0.0005° (yaklaşık 50m) fuzzing (sapma) uygulanmış `approximate_coordinates` projekte eder.
231
- 3. RLS politikaları doğrultusunda sadece public görünür ve modere edilmiş verileri anonim istemcilere açar.
232
-
233
- Bu sayede `app.current_tenant_id` ayarı `muni_a` olan bir kullanıcının sorgusu ne olursa olsun veritabanı seviyesinde `muni_b` satırlarına erişmesi engellenir.
234
-
235
- ---
236
-
237
- ## 3. Central Reverse Proxy ve API Yönlendirme Mimarisi
238
-
239
- KENTİM platformunda, istemciler (Vatandaş Web/Mobil, Belediye Paneli, Central Panel) ve belediye backend servisleri arasındaki API iletişimi, **Central Reverse Proxy (Merkezi Ters Proxy)** katmanı aracılığıyla güvenli, izole ve dinamik olarak yönetilir. Bu mimaride, belediyelerin backend sunucuları doğrudan internete maruz kalmaz; tüm trafik merkezi proxy üzerinden güvenli tünellerle yönlendirilir.
240
-
241
- ```mermaid
242
- sequenceDiagram
243
- autonumber
244
- actor Client as İstemci (Belediye Paneli / Vatandaş)
245
- participant Proxy as Central Reverse Proxy (api.kentim.com.tr)
246
- participant CentralDB as Central SaaS DB
247
- participant MuniBackend as Belediye Backend (muni_a_backend)
248
-
249
- Client->>Proxy: POST /v1/work-orders (Authorization: Bearer <JWT>)
250
- Proxy->>Proxy: 1. JWT imzasını doğrula ve çöz
251
- Proxy->>Proxy: 2. tenant_id (muni_a) ve rol bilgilerini ayıkla
252
- Proxy->>CentralDB: 3. Abonelik durumunu kontrol et (Lisans Aktif mi?)
253
- CentralDB-->>Proxy: Aktif (muni_a aktif lisans)
254
- Proxy->>Proxy: 4. RLS için app.current_tenant_id bağlamını set et
255
- Proxy->>MuniBackend: 5. İsteği güvenli tünelle (VPN) ilet (x-tenant-id: muni_a)
256
- MuniBackend->>MuniBackend: 6. PostgreSQL RLS & Sorgu kapsamını işlet
257
- MuniBackend-->>Proxy: 7. Yanıt Verileri (JSON)
258
- Proxy-->>Client: 8. HTTP 200 OK / 201 Created
259
- ```
260
-
261
- ### Anonim Public-Read Akış Diyagramı
262
- Central Reverse Proxy'nin JWT gerektirmeyen public okuma isteklerini nasıl yönettiğini gösteren süreç diyagramıdır:
263
-
264
- ```mermaid
265
- sequenceDiagram
266
- autonumber
267
- actor Client as Anonim İstemci (Vatandaş Web)
268
- participant Proxy as Central Reverse Proxy (api.kentim.com.tr)
269
- participant CacheDB as DB (public_map_cache)
270
- participant MuniBackend as Belediye Backend (muni_a_backend)
271
-
272
- Client->>Proxy: GET /v1/public/reports?municipality_id=muni_a&bbox=... (JWT'siz)
273
- Proxy->>Proxy: 1. Rate limit kontrol et (IP başına maks 60 req/min)
274
- Proxy->>CacheDB: 2. Önbellek kontrol et (tenant_id = muni_a AND bbox_hash)
275
- alt Önbellek İsabeti (Cache Hit)
276
- CacheDB-->>Proxy: Önbellekteki veriyi (fuzzing uygulanmış JSON) dön
277
- Proxy-->>Client: 200 OK (Cache-Control: max-age=30)
278
- else Önbellek Iskalama (Cache Miss)
279
- Proxy->>MuniBackend: 3. İstek yönlendir (x-tenant-id: muni_a)
280
- MuniBackend->>MuniBackend: 4. DB seviyesinde public_work_orders_view sorgula (fuzzing & masking uygulanır)
281
- MuniBackend-->>Proxy: 5. Yanıtı dön (JSON)
282
- Proxy->>CacheDB: 6. Sonucu unlogged cache tablosuna yaz (expires_at: now + 30s)
283
- Proxy-->>Client: 7. 200 OK (Cache-Control: max-age=30)
284
- end
285
- ```
286
-
287
-
288
- ### A. Central Reverse Proxy ve Tenancy Çözümleme Mantığı
289
- Tüm istemciler API isteklerini ortak ağ geçidi olan `https://api.kentim.com.tr` adresine gönderir. İletişim akışı şu şekildedir:
290
- 1. **Kimlik ve Kiracı Doğrulama (Auth & Tenancy Resolution):** Gelen her istekteki JWT token'ı proxy katmanında çözülür. Token içerisinden `tenant_id` (örn: `muni_a`) ve kullanıcının yetki rolü (örn: `muni_department_manager`) doğrulanır.
291
- - **JWT'siz Public Endpoint İstisnası:** `GET /v1/public/reports` endpoint'i JWT yetkilendirmesi gerektirmez. Kiracı tespiti URL sorgu parametrelerindeki `?municipality_id` veya `?city_slug` ile yapılır. Bu endpoint için IP başına 60 istek/dakika hız sınırı (rate limit) uygulanır. Yanıtlar sunucuda `Cache-Control: max-age=30` ile döner.
292
- 2. **Lisans / Abonelik Durum Denetimi (Licensing Gateway):**
293
- - Proxy, `tenant_id` üzerinden Central SaaS veritabanında lisans durumunu sorgular.
294
- - Lisans aktif ise istek yönlendirilir.
295
- - **402 Grace Period Durumu:** Lisansın süresi dolmuşsa ve belediye askıya alınmışsa; proxy gelen `POST`, `PUT`, `PATCH`, `DELETE` (yazma/güncelleme) isteklerini `402 Payment Required` hatası ile keser. Ancak, 7 günlük "grace period" (hoşgörü süresi) dahilinde, saha ekiplerinin iş tamamlaması için `pending_chief_approval` durumuna geçiş ve müdürlerin "Mevcut İşi Yeniden Atama (`re-assign`)" isteklerine özel olarak izin verir. `GET` (okuma) ve public harita (`GET /v1/public/reports`) istekleri grace period ve askıya alınma durumlarında da kesintisiz devam eder.
296
- 3. **Güvenli Yönlendirme (Secure Forwarding / Tunneling):**
297
- - Doğrulamadan geçen istekler, ilgili belediyenin Kubernetes iç ağındaki servisine (`http://muni-a-backend.internal:4000`) veya belediyenin kendi özel sunucusuna, proxy ile backend arasında kurulmuş güvenli bir VPN tüneli (örn: WireGuard/IPsec) üzerinden yönlendirilir. İsteğe `x-tenant-id: muni_a` header'ı eklenir.
298
-
299
- > [!WARNING]
300
- > **JWT'siz Public Endpoint Güvenlik Uyarısı:** `GET /v1/public/reports` endpoint'i JWT token korumasına tabi tutulmadığı için kasıtlı olarak veritabanı view katmanı (`public_work_orders_view`) ile sınırlandırılmıştır. Ham tablolara anonim erişim kesinlikle engellenmiştir ve view seviyesindeki PostGIS fuzzing ile RLS koruması devrededir.
301
-
302
-
303
- ### B. Belediye Paneli ve API İletişimi (Panel-to-Backend Proxying)
304
- * **Tek Ön Yüz Uygulaması (Shared Frontend):** Belediye Paneli (`panel.kentim.com.tr`) tüm belediyelerin ortak kullandığı tek bir web uygulamasıdır. Giriş yapan personelin tarayıcısına yüklenen ön yüz, arka planda hangi belediyeye ait olduğunu sadece JWT içindeki `tenant_id` sayesinde bilir.
305
- * **Direct API Maskeleme:** Belediye paneli, belediyelerin gerçek API sunucu adreslerini (IP veya özel URL'lerini) asla bilmez. Tüm istekler `api.kentim.com.tr` proxy'sine gönderildiği için belediye altyapısı doğrudan internetten gelebilecek saldırılara (DDoS, Brute Force, Port Taraması) karşı tamamen maskelenmiştir.
306
-
307
- ### C. Central Admin ve Belediye Backend İletişimi (Central-to-Backend Command Pipeline)
308
- Central Admin (`central.kentim.com.tr`) panelinden tetiklenen global komutların (örn: **Afet Modu** veya **Heartbeat Sağlık Kontrolü**) belediye backend'lerine güvenli bir şekilde iletilmesi için çift yönlü bir iletişim boru hattı kurulmuştur:
309
-
310
- 1. **Belediye -> Central (Push & Heartbeat):**
311
- - **Heartbeat:** Belediye backend'leri her 1 dakikada bir Central API'ye `/v1/central/heartbeat` uç noktası üzerinden sinyal gönderir. Central Admin bu sayede sistem sağlığını izler.
312
- - **İstatistik Push:** Her belediye 15 dakikada bir veritabanından derlediği anonimleştirilmiş iş/SLA istatistiklerini Central API'ye push eder.
313
- 2. **Central -> Belediye (Secure Command Loopback):**
314
- - Central Admin, seçili bir belediyede afet modunu tetiklemek veya lisans durumunu zorla güncellemek istediğinde, Central Proxy ilgili belediye backend'inin kayıtlı **"Command Loopback"** adresine istek gönderir.
315
- - Bu isteklerin güvenliği, Central ve Belediye backend'i arasında önceden paylaşılmış olan **HMAC-SHA256 Master Key** (API Secret) ile sağlanan bir imza doğrulama adımıyla (`x-kentim-signature`) korunur. Belediye backend'i imzayı doğrulamadan komutu çalıştırmaz.
316
-
317
- ---
318
-
319
- ## 4. Çok Belediyeli Operasyonel Kurallar & Çakışma Yönetimi
320
-
321
- Birden fazla belediyenin aynı anda aktif çalışması nedeniyle oluşabilecek çakışmalar (conflict) şu kurallarla çözülmüştür:
322
-
323
- ### 1. Akıllı Konum Tabanlı Yönlendirme (Geofencing Router)
324
- * Vatandaş bir sorun raporladığında, koordinatlar (GPS) paylaşılan **Tenant Router** motoruna gönderilir.
325
- * Sistem, basit bir bounding box yerine doğrudan hassas sınır poligonunu kullanarak, koordinatın hangi belediyenin PostGIS sınır poligonu (`boundaries`) içerisinde yer aldığını **`ST_Contains`** fonksiyonu ile coğrafi veritabanı düzeyinde anlık tespit eder.
326
- * Tespit edilen belediyenin `tenant_id` değeri rapora otomatik işlenir ve o belediyenin moderasyon kuyruğuna aktarılır.
327
- * **Sınır Çakışması & Yetki Ayrım Kuralı (Overlap & Category-Based Routing):** Büyükşehir belediyesi ve ilçe belediyesi gibi sınırları tamamen örtüşen/çakışan yetki alanlarında sistem şu akıllı mekanizmaları işletir:
328
- 1. **Kategori Bazlı Akıllı Yönlendirme:** Yönlendirme motoru sadece koordinata değil, seçilen **kategoriye** de bakar. Örneğin, çakışan alanda "Ana Arter Temizliği/Bakımı", "Metro Hatları", "Su/Kanalizasyon Şebekesi" gibi büyükşehir yetkisindeki kategoriler doğrudan Büyükşehir Belediyesi `tenant_id`'sine; "Sokak Çöpü", "Mahalle Parkı Bakımı" gibi ilçe yetkisindeki kategoriler ise İlçe Belediyesi `tenant_id`'sine otomatik yönlendirilir.
329
- 2. **Belediyeler Arası Sevk (Cross-Tenant Referral):** Eğer bir rapor sistemsel olarak yanlış belediyeye atanırsa, ilgili belediyenin moderatörü raporu reddetmek yerine tek butonla çakışan diğer belediyeye (örn: İlçeden Büyükşehire) sevk edebilir. Bu durumda rapor eski belediyede referanslı kapatılarak, yeni belediyenin moderation kuyruğuna asenkron olarak aktarılır. Böylece vatandaşın şikayeti iptal edilmemiş olur.
330
- 3. **Fallback:** Kategoriden ayrım yapılamayan veya tam çakışan belirsiz koordinatlarda `central_admin`'in `priority_order` (öncelik sırası) değeri referans alınır. Birden fazla belediyenin tam çakışması ve çözülememesi durumunda koordinat manuel sevk edilmek üzere `central_admin` Disputes havuzuna düşer. Sınır poligonları `central_admin` tarafından haritadan dinamik olarak yönetilir.
331
- * **İstisna**: Acil Durum / Panik Butonu sinyalleri, vatandaşın tahliye sırasında sınır dışına çıkabilmesi ihtimali nedeniyle coğrafi doğrulama engelinden (sınır poligonu kontrolünden) muaftır; en yakın koordinattaki belediyeye kriz masası sinyali iletilir.
332
- * **Panik Butonu DDoS Koruması**: Panik butonu rate limiting kurallarından (IP bazlı) muaftır; istismarı önlemek amacıyla **Device ID doğrulama** zorunludur. Aynı Device ID'den dakikada maksimum **5 panik sinyali** kabul edilir; limiti aşan istekler HTTP 429 ile reddedilir (vatandaşa görünmez, sessizce). Device ID doğrulama `/devices/validate` endpoint'iyle yapılır.
333
-
334
- ### 2. Küresel Kimlik Yapısı (Global UUID Mimarisi)
335
- * Sistemde hiçbir kiracıda sıralı tamsayı (auto-increment ID) kullanılmaz.
336
- * Tüm iş emirleri, raporlar ve kullanıcılar **UUIDv4** formatında küresel benzersiz kimliklere sahiptir.
337
- * Bu sayede, merkezi veri analizlerinde veya merkezi raporlama ekranlarında veriler çakışmadan toplulaştırılabilir (aggregation).
338
-
339
- ### 3. Bağımsız Çalışma Takvimleri (SLA & Business Hours)
340
- * Her belediyenin (ve hatta müdürlüğün) kendi çalışma takvimi, resmi tatil tanımları ve vardiya saatleri bağımsızdır.
341
- * SLA sayaçları hesaplanırken, iş emrinin bağlı olduğu `tenant_id` üzerinden ilgili belediyenin yerel çalışma takvimi referans alınır.
342
- * **Kritik Kural**: `Acil` veya `Kriz` işaretli iş emirlerinde yerel çalışma saati dikkate alınmaz, 7/24 kesintisiz SLA işletilir.
343
-
344
- ### 4. Lisans Durdurma / Askıya Alma Modu
345
- * Abonelik süresi biten bir belediyenin arayüzleri **kısıtlı salt-okunur** moda çekilir.
346
- * Bu durum diğer belediyelerin operasyonunu asla etkilemez.
347
- * Askıya alınan belediyenin saha ekipleri, ellerindeki devam eden işleri tamamlayabilmek için **7 günlük "salt-tamamlama" (grace period)** modunda çalışabilir. Bu süre zarfında mevcut işlerin saha koordinasyonunu sağlamak amacıyla **"Mevcut İşi Yeniden Atama (Re-assign)"** işlemine izin verilir, ancak sisteme sıfırdan yeni iş emri veya vatandaş raporu kabul edilmez.
348
-
349
- ### 5. Dinamik IoT Payload Dönüştürücü ve Telemetri İzolasyonu
350
- * **Merkezi Geçit (SaaS Gateway) Katmanı:** Ortak uç noktaya (`/v1/integrations/iot/telemetry`) gelen tüm istekler Central SaaS Gateway tarafından karşılanır. İstekteki `Authorization` başlığındaki API anahtarı çözülerek hedeflenen belediyenin `tenant_id` değeri saptanır.
351
- * **Dinamik Dönüştürme Motoru (Dynamic Payload Transformer):** Saptanan belediyenin izole veritabanı şemasındaki en güncel "Payload Mapping" kuralları veya "Custom Parser Script" kod bloğu sunucu üzerindeki paylaşımlı önbellek bellek katmanından (PostgreSQL Shared Buffers & Index Caching) hızlıca çekilir. **Önbellek Invalidation Kuralı:** Admin entegrasyon ayarını kaydettiğinde ilişkili yapılandırma verileri anında güncellenir ve veritabanı belleği invalidation'a uğramadan güncel veriyle senkronize olur. Cihazdan gelen ham JSON verisi, belediyenin kendi tanımladığı bu parser fonksiyonundan geçirilerek KENTİM standart şemasına (UUID, doluluk, arıza durumu, PostGIS koordinatları) dönüştürülür.
352
- * **V8 Sandbox Kısıtları & Fastify API Decoupling:** Dynamic Payload Transformer'ın ham veriyi JS ile parse ettiği V8 sandbox motoru için şu kısıtlar geçerlidir: Payload boyutu max 10KB, maks çalışma süresi 50ms, maks bellek 16MB, ağ ve dosya sistemi erişimi yasak, sadece saf hesaplama işlemleri izinlidir.
353
-
354
- > **⚠️ V8 Sandbox ile İlgili Kritik Operasyonel Risk:**
355
- > Belediyelerin kendi JavaScript parser scripti yazabilmesi, uzun vadede ciddi güvenlik ve bakım riski oluşturur. MVP aşamasında sandbox kısıtları (10KB / 50ms / 16MB) ile bu risk kısmen kontrol altına alınmıştır. Ancak script versiyonlama, kod inceleme (code review), rollback ve hata ayıklama mekanizmaları MVP'de sınırlıdır. Bu konu, "V8 Sandbox Yönetim Riski" olarak bilinçli kabul edilmiştir (bkz. yukarıdaki risk tablosu).
356
- **Fastify API Asenkron Çözümleme Hattı (Decoupling):** Yoğun telemetri trafiğinin veya parser CPU kilitlenmelerinin ana Fastify API Gateway thread'ini dondurmasını engellemek amacıyla, parser motorunun çalıştırılması API thread'inden tamamen ayrıştırılmıştır. Gelen ham telemetri verileri anlık olarak **PostgreSQL unlogged tablosu** (`iot_telemetry_queue`) üzerine `INSERT` ile yazılır, ardından `pg_notify('iot_telemetry_channel', device_id)` komutuyla worker'lara sinyal gönderilir ve client'a `202 Accepted` dönülür. Kuyruktaki veriler, ana API'den bağımsız olarak `LISTEN iot_telemetry_channel` dinleyen **asenkron Warm Worker Node.js process'leri** tarafından arka planda sırayla `FOR UPDATE SKIP LOCKED` ile tüketilerek KENTİM standart şemasına dönüştürülür. **Mimari Karar:** Redis kullanılmaz; tüm asenkron kuyruk iletişimi PostgreSQL LISTEN/NOTIFY mekanizması üzerinden yönetilir.
357
- * **PostgreSQL RLS Koruması:** Standart hale getirilen veri, veritabanı katmanında `app.current_tenant_id` bağlamı ile kaydedilir. Her belediyenin telemetri geçmişi, RLS politikaları doğrultusunda diğer belediyelerin erişiminden tamamen izole edilmiş bir şekilde kendi şemasında (`schema_muni_x`) saklanır.
358
- * **6. Halka Açık Harita Önbellek ve Invalidation Politikası:**
359
- İstemcilerin gönderdiği `/v1/public/reports` istekleri için PostgreSQL üzerinde WAL yazmayan ultra hızlı unlogged `public_map_cache` tablosu (`tenant_id` + `bbox_hash` bazlı, 30 saniye TTL) kullanılmaktadır.
360
- **Statik-Dinamik Veri Ayrıştırma & CDN Optimizasyonu:** Yoğun trafik altında harita cache invalidation ve CDN purge maliyetlerini engellemek amacıyla, **Harita Pin Verileri (koordinat, kategori, durum)** ile **Detay Verileri (fotoğraflar, destek oyları, dahili notlar)** API seviyesinde tamamen ayrıştırılır.
361
- 1. Harita ilk yüklendiğinde ve kaydırıldığında çağrılan `GET /v1/public/reports` endpoint'i yalnızca hafif pin koordinatlarını döndürür ve 30 saniye boyunca CDN ve unlogged cache seviyesinde agresif olarak önbelleğe alınır.
362
- 2. Vatandaş bir pine tıkladığında açılan detay pop-up/drawer'ı için ayrı bir `GET /v1/public/reports/:public_id` endpoint'i çağrılır. Bu endpoint asenkron/debounced önbellekten güncel `support_count` ve fotoğraf URL'lerini çeker.
363
- Bu sayede haritaya gelen her tekil "+1" desteği veya fotoğraf gizleme işlemi sonrasında tüm harita koordinat önbelleği ve CDN geçersiz kılınmak (purge edilmek) zorunda kalmaz. DB ve network maliyeti %90 oranında düşürülür.
364
- **Mevcut Durum:** Bu altyapı ve cache invalidation mekanizmaları ile CDN purge entegrasyonu tamamen tanımlanıp sisteme entegre edilmiştir.
365
-
366
- > **⚠️ Public Harita Ölçeklenebilirlik Riski (Kabul Edilmiş):**
367
- > 30 saniyelik unlogged cache + BBOX bazlı dinamik yükleme + Leaflet clustering yaklaşımı, orta ölçekli belediyeler için yeterlidir. Ancak Türkiye geneli açılış + sokak seviyesine kadar zoom senaryosunda (özellikle büyükşehir belediyeleri) ciddi performans riski taşır.
368
- > MVP aşamasında bu risk kabul edilmiştir. Erken yük testi ve gerekirse vector tile / pre-aggregate katman geçişi MVP sonrası planlanmalıdır.
369
-
370
- *MVP Sürümü — KENTİM Mimari Belgesi*
371
-
372
- ## EK: Public Harita — Veritabanı, RLS ve Reverse Proxy Değişiklikleri
373
-
374
- ### Belediye Veritabanı / Şema Güncellemeleri
375
- - `work_orders` tablosuna eklenecek alanlar: `is_public_visible BOOLEAN DEFAULT TRUE`, `public_hidden_reason TEXT NULL`, `public_hidden_at TIMESTAMPTZ NULL`, `public_hidden_by UUID NULL`, `boundary_version_id UUID (ref: municipal_boundary_versions.id)`, **`priority VARCHAR(10) NOT NULL DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high', 'urgent', 'crisis'))`** (düşük/normal/yüksek/acil/kriz öncelik seviyeleri).
376
- - Yeni tablo: `municipal_boundary_versions(id UUID PRIMARY KEY DEFAULT gen_random_uuid(), municipality_id UUID, boundaries GEOMETRY(MultiPolygon, 4326) NOT NULL, active_from TIMESTAMPTZ NOT NULL, active_to TIMESTAMPTZ NULL, created_at TIMESTAMPTZ DEFAULT NOW())`. **Not:** Belediye sınırları birden fazla bağlantısız coğrafi parçadan (adalar, enklav bölgeler) oluşabileceğinden `MultiPolygon` tipi kullanılmaktadır.
377
- - Yeni tablo: `support_votes(id UUID, work_order_id UUID, citizen_id UUID, created_at TIMESTAMPTZ)` with UNIQUE(work_order_id, citizen_id).
378
- - Denormalize: `work_orders.support_count INTEGER DEFAULT 0` — row-locking darboğazını engellemek için asenkron debounced worker ile güncellenmektedir.
379
-
380
- ### PostgreSQL RLS — Public Read Policy
381
- - Yeni policy: anonim GET istekleri için `public_map_read_policy`; yalnızca `is_public_visible = TRUE` ve `status NOT IN ('pending','rejected')` dönecek.
382
- - `public_work_orders_view` view'u uygulama için yalnızca güvenli alanları projekte edecek; koordinatlar ±0.0005° (≈50m) fuzzing uygulanmış `approximate_coordinates` şekilde dönecek. **`photo_urls` Alanı:** View içinde `photo_urls` alanı, `work_order_photos` tablosuna JOIN yapılarak `is_public_visible = TRUE` ve silinmemiş (aktif) fotoğraflar JSONB array olarak aggregate edilir. Gizlenmiş veya silinmiş fotoğraflar view çıktısına yansıtılmaz.
383
-
384
- ### Central Reverse Proxy — Yeni Public Endpoint
385
- - `GET /v1/public/reports` JWT gerektirmez; proxy middleware zinciri: JWT atlanır, tenant tespiti `?municipality_id` veya `?city_slug` ile yapılır, IP başına 60 req/min rate limit uygulanır, yanıt `public_work_orders_view` üzerinden döner, `Cache-Control: max-age=30` eklenir.
386
- - `POST /v1/public/reports/:id/support` ise JWT zorunlu (hesaplı kullanıcılar için +1 desteği).
387
-
388
- ### Lisans / 402 Grace Period İstisnası
389
- - `GET /v1/public/reports` endpointi 402 (abonelik bitiş) durumunda bloke edilmez; yalnızca yazma işlemleri engellenir.
390
-
391
- ### Public Cache Invalidation Kuralı (Operasyonel)
392
- - Bir iş emrinin `status`, `is_public_visible` veya `support_count` alanı değiştiğinde ilgili tenant'ın `public_map_cache` tablosundaki tüm önbellek satırları transactional delete ile temizlenir.
393
-
394
- Bu eklemeler public harita performansı, KVKK uyumu ve anonim okuma güvenliği için zorunludur.
395
-
396
- ### EK - JWT'siz Public Endpoint Güvenlik Notu ve Anonim Akış
397
-
398
- 1) Güvenlik Notu (Section 3A genişletme)
399
- - `GET /v1/public/reports` JWT gerektirmese bile ham tabloya doğrudan erişim **yoktur**. Proxy sadece `public_work_orders_view` view'unu sunar; kişisel veri proje seviyesinde view ile dışlanır. Bu istisna, proxy katmanında özel rate-limit, IP kısıtlaması ve response projection ile korunur. Dokümana açık bir güvenlik uyarısı eklenecektir.
400
-
401
- 2) Anonim Public-Read Akış Diyagramı (Section 3 Sequence Diagram genişletme)
402
- - Central Reverse Proxy için ayrı bir JWT'siz akış diyagramı eklenmelidir. Kısa akış:
403
- - Client -> Proxy: `GET /v1/public/reports?municipality_id=...&bbox=...` (Anonim)
404
- - Proxy: tenant tespiti (query param), rate-limit kontrolü, cache (public_map_cache) kontrolü
405
- - Proxy -> MuniBackend: `SELECT` via `public_work_orders_view` (db-level projection)
406
- - Proxy -> Client: 200 OK + maskelenmiş `public_work_orders_view` verisi
407
-
408
- Bu not ve diyagram eklentisi belgenin güvenlik bölümüne entegre edilecektir.
409
-
410
- ---
411
-
412
- ## Mimari Dayanıklılık, Önbellek Hata Yönetimi ve Asenkron İşlem Mimarisi
413
-
414
- > **Mimari Karar:** KENTİM'de Redis kullanılmaz. Tüm asenkron kuyruk, bildirim ve IPC iletişimi PostgreSQL'in yerel **LISTEN/NOTIFY** mekanizması ve **unlogged tablolar** aracılığıyla yönetilir. Bu karar; bağımlılığı azaltır, belediye backend'lerinin "indir–ayarla–çalıştır" kurulum modeliyle tam uyumlu olmasını sağlar ve Redis yönetimi gerektirmeyen operasyonel sadeliği korur.
415
-
416
- > **⚠️ Kritik Mimari Risk (Kabul Edilmiş):**
417
- > LISTEN/NOTIFY + unlogged tablo stratejisi, sistemin **en önemli operasyonel risklerinden biridir**. Özellikle afet modu, viral şikayet dalgası veya aynı anda binlerce destek oyu gibi yüksek eşzamanlılık senaryolarında şu riskler mevcuttur:
418
- > - PostgreSQL connection pool'unun LISTEN bağlantıları tarafından tüketilmesi
419
- > - `pg_notify` mesajlarının kaybolması (notify storm / queue overflow)
420
- > - Worker process'lerinin yeniden bağlanamaması durumunda kuyruk birikmesi
421
- >
422
- > Bu risk **MVP aşamasında bilinçli olarak kabul edilmiştir**. Detaylı mitigation stratejileri ve kapasite test zorunlulukları D4 bölümünde tanımlanmıştır. Üretim ortamına alınmadan önce her belediye backend'inin D4'te belirtilen yük testlerini geçmiş olması **zorunludur**.
423
-
424
- > **Önerilen Ek Mitigation (V1):** Kritik kuyruklar (panic, dispute, notification DLQ) için V1 sürümünde hafif bir managed queue (Amazon SQS, RabbitMQ veya Cloud Tasks) ile hibrit mimariye geçilmesi önerilir. Detaylar için `proje.md` → KENTİM Sürüm Yol Haritası bölümüne bakınız.
425
-
426
- Sistem mimarisi, yüksek yük ve kesinti senaryolarına karşı operasyonel sürekliliği korumak amacıyla şu hata yönetimi ve yedekleme (resilience) modelleriyle donatılmıştır:
427
-
428
- ### 1. Önbellek Iskalama Davranışı ve Doğrudan Veritabanı Fallback Akışı (Cache Miss Fallback)
429
- * İstemci `GET /v1/public/reports` isteği gönderdiğinde Central Proxy `public_map_cache` tablosunda `bbox_hash` kontrolü yapar.
430
- * **Cache Miss (Önbellek Iskalama) Durumu:**
431
- 1. Proxy, isteği doğrudan ilgili belediyenin backend'ine VPN tüneli üzerinden yönlendirir.
432
- 2. Belediye backend'i `public_work_orders_view` üzerinden veri tabanını RLS kurallarıyla sorgular.
433
- 3. Alınan veri istemciye döndürülürken eşzamanlı olarak asenkron bir `Promise` (arka planda, istek yanıtını geciktirmeden) ile unlogged `public_map_cache` tablosuna 30 saniyelik TTL ile yazılır.
434
- * **Veritabanı Koruma Bariyeri (Mutex Lock):** Çok yüksek eşzamanlı istek altında aynı `bbox_hash` için önbellek bulunamadığında veritabanının kilitlenmesini engellemek için **Single Flight / Mutex Lock** deseni uygulanır. Aynı anda gelen özdeş 100 sorgudan sadece 1 tanesi veritabanına gönderilir, diğer 99 istek bu sorgunun dönmesini ve önbelleğe yazılmasını bekleyerek doğrudan önbellekten yanıt alır.
435
-
436
- ### 2. Asenkron `support_count` Debouncer Mimarisi (PostgreSQL Tabanlı)
437
- `support_votes` tablosuna gelen yoğun "+1" oylarının `work_orders.support_count` sayacına row-locking yaratmadan yansıtılması için **tamamen PostgreSQL tabanlı debounce mimarisi** kullanılmaktadır:
438
- * **Kuyruk Tablosu:** `support_count_queue` adlı **unlogged tablo** kullanılır. Her "+1" oyu bu tabloya `INSERT` ile yazılır; INSERT işlemi row-lock oluşturmaz.
439
- * **Worker Tetikleme:** `INSERT` sonrasında `pg_notify('support_count_channel', work_order_id)` ile worker Node.js process'i tetiklenir.
440
- * **Debounced Bulk UPDATE:** `LISTEN support_count_channel` dinleyen worker, gelen bildirimleri **300ms** bekler (debounce), ardından biriktirilen tüm `work_order_id`'leri tek bir `UPDATE work_orders SET support_count = (SELECT COUNT(*) FROM support_votes WHERE work_order_id = id) WHERE id = ANY(:ids)` bulk sorgusuyla işler.
441
- * **Bellek Koruması (Fallback):** Worker'ın beklenmedik şekilde yeniden başlaması durumunda işlenemeyen kuyruk satırları `support_count_queue` tablosunda kalır ve worker tekrar ayağa kalktığında otomatik olarak işlenir. Maksimum 10,000 işlenmemiş satır biriktiğinde worker bulk flush gerçekleştirir.
442
- * **İzleme:** Tüm batch işlemler `SUPPORT_COUNT_FLUSH` log tipiyle Pino logger'a yazılır.
443
-
444
- ### 3. Dinamik Özel Raporlama ve Sayfalama (Pagination) Altyapısı
445
- * **Read-Replica İzolasyonu:** Belediye yöneticileri tarafından **Custom Report Builder** ile yapılan dinamik raporlama ve sorgulamalar, operasyonel veritabanına ek yük bindirmemek amacıyla **salt-okunur veritabanı kopyaları (Read-Replicas)** üzerinden yürütülür.
446
- * **Merkezi Konsolidasyon ve SaaS Analitik DB:** Central SaaS panelindeki **Yönetici Sunum Raporları (Executive Presentation-Ready Reports)** için sorgulamalar, tenant'lar arasındaki RLS sınırlarını (cross-tenant leak) ihlal etmemek adına ham belediye veritabanlarına doğrudan dokunmaz. Arka planda çalışan asenkron bir **ETL Scheduler** (cron / event-driven worker) servisi, her belediye şemasındaki verileri anonimleştirip agrege ederek Central SaaS veritabanındaki `central_analytics_metadata` tablosuna aktarır. Sunum raporları doğrudan bu merkezi analitik tablodan beslenir.
447
- * **Veri Sayfalama (Sayfalama - Pagination) Zorunluluğu:** Platformdaki tüm listeleme endpoint'lerinde (`GET /v1/worker/tasks`, `GET /v1/admin/audit-logs`, `GET /v1/admin/reports/chronic` vb.) ve Leaflet harita veri pin çekme akışlarında, bellek tükenmesini engellemek için **sayfalama (pagination)** kullanımı zorunludur. Performans ve ölçeklenebilirlik için:
448
- - **Cursor-based Pagination (Keşifsel Sayfalama):** Sürekli güncellenen anlık listelerde (örn: iş emri kuyruğu) mükerrer kayıt veya satır atlama sorununu önlemek için `next_cursor` (UUID veya timestamp) ve `limit` parametreleri kullanılır.
449
- - **Offset-based Pagination (Sayfa Bazlı Sayfalama):** Denetim logları gibi statik tarihsel aramalarda `page` ve `limit` parametreleri tercih edilir.
450
- - **Leaflet BBOX Sayfalama:** Leaflet Map viewport sınırlarına göre yapılan coğrafi sorgulamalarda, tek seferde çekilen pin sayısı `limit` parametresiyle sınırlanır ve harita hareket ettikçe dinamik olarak chunk'lar halinde yüklenir.
451
-
452
- ### Kritik Mimari Risklerin Özeti (MVP Kapsamı)
453
-
454
- Aşağıdaki riskler, **"MVP'yi hızlı ve basit şekilde dağıtılabilir kılmak"** hedefiyle bilinçli olarak kabul edilmiştir. Her birinin gerekçesi ve etkisi aşağıda belirtilmiştir.
455
-
456
- | Risk Alanı | Kabul Seviyesi | Gerekçe (Neden Kabul Ediyoruz?) | Yüksek Yükteki Etki | Mevcut Kontrol | V1 Önerisi |
457
- |-----------------------------------------|----------------|----------------------------------|---------------------|----------------|------------|
458
- | PostgreSQL LISTEN/NOTIFY + Unlogged | Bilinçli Kabul | Redis bağımlılığını ortadan kaldırarak belediye dağıtımını basitleştirmek | Connection exhaustion, notify kaybı, kuyruk birikmesi | Debounce + bulk processing, D4 kapasite test zorunluluğu | Kritik kuyruklar için hibrit managed queue |
459
- | Tamamen PostgreSQL bağımlılığı | Bilinçli Kabul | Operasyonel basitlik ve tek veritabanı avantajı | Tek hata noktası, ölçeklenebilirlik sınırlaması | Read replica + unlogged tablo tasarımı | Önemli kuyruklar için SQS/RabbitMQ |
460
- | Belediye backend'lerinin basit dağıtımı | Bilinçli Kabul | "İndir .env çalıştır" modelini mümkün kılmak | Operasyonel olgunluğu düşük belediyelerde ciddi sorun | D1-D5 operasyonel standartlar | CI/CD pipeline + artifact signing + monitoring |
461
- | V8 Sandbox ile custom parser | Bilinçli Kabul | Belediyelere esnek IoT entegrasyonu sunmak | Güvenlik açığı, bakım maliyeti, kod kalitesi sorunu | 10KB / 50ms / 16MB limit + DLQ | Parser script için approval + versiyonlama süreci |
462
-
463
- **Sonuç:** KENTİM SaaS mimarisi, yüksek trafik ve altyapı kesintilerine karşı büyük ölçüde dayanıklı hale getirilmiştir. Ancak operasyonel başarı; multi-tenant migration disiplini, secret rotasyon prosedürleri, düzenli restore drill'leri ve LISTEN/NOTIFY kapasite testlerine sıkı bağlıdır. Yukarıdaki riskler MVP aşamasında **"basit dağıtım"** hedefiyle bilinçli olarak kabul edilmiştir.
464
-
465
- ---
466
-
467
- ## 5. Belediye Sunucusu Hızlı Dağıtım ve Central Sağlık Monitör Standartları
468
-
469
- ### A. Belediye Backend Hızlı Kurulum Modeli (indir .env ayarla çalıştır)
470
-
471
- **⚠️ Önemli Uyarı:** Bu model, teknik olarak mümkün olsa da **gerçek üretim ortamlarında operasyonel olgunluk gerektirir**. Aşağıda tanımlanan kurulum adımları, gerekli altyapı ve süreç disiplini olmadan ciddi riskler doğurabilir.
472
-
473
- Her belediye backend servisi, Fastify çatısı altında standartlaştırılmış bir Node.js üretim paketi olarak sunulur. Kurulum süreci şu adımlardan ibarettir:
474
-
475
- 1. **İndir ve Kurulum**
476
- 2. **.env Yapılandırması** (PostgreSQL + Central bağlantıları)
477
- 3. **Süreç Yönetimi** (PM2 veya systemd)
478
-
479
- Ancak bu basit kurulumun **gerçek hayatta çalışabilmesi** için belediyenin aşağıdaki yetkinliklere sahip olması zorunludur:
480
-
481
- - PostgreSQL veritabanı yönetimi ve yedekleme
482
- - SSL/TLS sertifika yönetimi
483
- - Sistem güncellemeleri ve güvenlik yaması takibi
484
- - Log izleme ve alert mekanizması
485
- - Secret rotation ve anahtar yönetimi disiplini
486
-
487
- **MVP Kapsamında Kabul Edilen Risk:**
488
- Bu model, "her belediye kolayca kendi sunucusunu çalıştırabilsin" vizyonuyla tasarlanmıştır. Ancak operasyonel olgunluğu düşük olan belediyelerde bu yaklaşım **teknik borç ve güvenlik açığı** yaratabilir. Bu risk, D1-D5 operasyonel standartlar ile kısmen kontrol altına alınmıştır.
489
-
490
- ### B. Merkezi Sistem Sağlığı İzleme ve Polling Mimarisi (Central Health Monitor)
491
- Merkezi SaaS Sağlık Motoru (`Central Health Monitor Engine`), hem merkez altyapısının hem de dağıtık belediye sunucularının durumunu 1 dakikalık döngülerle izler:
492
- 1. **Belediye `/ready` Polling (Aktif Sorgulama):** Central Health Monitor, Central DB'deki tüm aktif belediyelerin IP/Domain adreslerini sorgular ve her birinin `/ready` endpoint'ine `HMAC-SHA256` imzalı (`x-kentim-signature`) HTTP GET istekleri atarak veritabanı ve disk doluluk durumlarını sorgular.
493
- 2. **Belediye `/heartbeat` Push (Pasif Takip):** Belediyeler ayrıca `/v1/central/heartbeat` üzerinden 1 dakikalık periyotlarla central sunucuya push sinyali gönderir. Central Monitor, son 3 dakika boyunca heartbeat sinyali göndermemiş ve polling isteğine yanıt vermemiş belediye backend'lerini **"UNREACHABLE / DOWN"** durumuna alır ve central_admin paneline e-posta/push uyarı bildirimi gönderir.
494
-
495
-
496
- ### C. Merkezi Onay ve Giriş Kilidi (SaaS Lockout Pipeline)
497
- Belediyenin sistemde panel erişimine sahip olabilmesi, Central Admin tarafından verilecek **"Aktif Onay"** şartına bağlıdır:
498
- 1. **Giriş Engeli Mantığı:** Belediye personeli (müdür, şef, moderatör vb.) belediye paneline giriş yapmaya (`POST /v1/auth/login`) çalıştığında, belediye backend auth modülü JWT üretmeden önce central tenant registry servisinden veya yerel lisans/aktivasyon cache'inden ilgili belediyenin durumunu sorgular.
499
- 2. **Aktivasyon Kontrolü:** Eğer belediyenin Central DB'deki durumu `active` (onaylı) değilse (örn: `pending_approval` veya `passive` ise), giriş isteği anında kesilerek HTTP `403 Forbidden` ve `MUNICIPALITY_NOT_APPROVED` ("Belediyeniz henüz merkez tarafından onaylanmamıştır veya aboneliği askıdadır.") hata koduyla reddedilir. Bu sayede merkezden onay almayan belediyelerin paneline hiçbir personel giriş yapamaz.
500
-
501
- ### D. Operasyonel Dayanıklılık, Migration ve Güvenlik Operasyonları (Yeni — 2026-05)
502
-
503
- "İndir, `.env` ayarla, çalıştır" modelinin **gerçek dünyada güvenli ve sürdürülebilir** olabilmesi için aşağıdaki operasyonel standartlar zorunludur:
504
-
505
- #### D1. Multi-Tenant Şema Migration Stratejisi
506
- - **Araç:** Sqitch veya özel Node.js migration runner (transactional).
507
- - **Politika:**
508
- - Her migration, **Central DB + tüm aktif municipal şemalar** üzerinde sırayla ve idempotent çalışmalıdır.
509
- - Migration'lar **blue-green** veya **shadow table** tekniğiyle uygulanır; asla `ALTER TABLE` doğrudan production'da kilit oluşturmamalıdır.
510
- - 50+ belediye varsa migration'lar **batch** (örneğin 10'ar 10'ar) ve 5-10 dakika aralıklarla çalıştırılır.
511
- - Her belediye backend'i ayağa kalktığında `SELECT * FROM schema_migrations WHERE version = 'xxxx'` kontrolü yapar; eksik migration varsa **startup'ta 503** ile kendini rapor eder (Central Health Monitor bunu yakalar).
512
- - **Rollback:** Her migration'ın ters script'i zorunludur. Central Admin panelinden "Migration'ı Geri Al" butonu sadece `muni_admin` + 2FA ile tetiklenebilir.
513
-
514
- #### D2. Secret ve Anahtar Rotasyon Politikası
515
- | Anahtar Türü | Rotasyon Sıklığı | Mekanizma | Sorumlu |
516
- |---------------------------|------------------|------------------------------------------------|------------------|
517
- | HMAC_MASTER_KEY | 90 gün | Central → Belediye'ye yeni key + 48 saat overlap | central_admin |
518
- | JWT_SECRET (belediye) | 180 gün | Belediye admin panelinden manuel rotasyon + tüm refresh token'ların invalidasyonu | muni_admin |
519
- | API_KEY (IoT / entegrasyon) | 365 gün veya ihlal durumunda | `/central/municipalities/:id/api-key/rotate` (eski key anında geçersiz) | central_admin + muni_admin |
520
- | CENTRAL_API_KEY | 180 gün | Central yeni key üretir, belediyeye güvenli kanal ile iletir | central_admin |
521
-
522
- - **Zorunlu:** Tüm rotasyonlar `central_audit_logs` ve ilgili belediyenin `audit_logs` tablosuna `KEY_ROTATION` eventi olarak yazılır.
523
- - **Acil İhlal Prosedürü:** HMAC veya JWT secret sızıntısı şüphesinde Central 15 dakika içinde tüm ilgili belediyelere yeni key push'lar ve eski key'leri reddeder.
524
-
525
- #### D3. Backup, Disaster Recovery ve Veri Saklama
526
- - **Central SaaS DB:** Günlük tam yedek + 15 dakikalık WAL archiving (PITR). 35 gün retain.
527
- - **Her Belediye Şeması:**
528
- - Haftalık mantıksal dump (`pg_dump --schema-only` + veri).
529
- - Günlük fiziksel base backup + WAL.
530
- - **Cross-region** replika önerilir (aynı sağlayıcı içinde farklı availability zone).
531
- - **Restore Testi:** Her 90 günde bir **otomatik restore drill** çalıştırılır (Central Health Monitor tarafından tetiklenir). Başarısız drill → `muni_admin` ve `central_admin`'e kritik uyarı.
532
- - **KVKK Cold Storage:** 3 yılı aşan metin/loglar S3 Glacier veya eşdeğer "WORM" (Write Once Read Many) depolamaya taşınır. Silme talebi geldiğinde hem aktif DB hem cold storage taranır.
533
-
534
- #### D4. PostgreSQL LISTEN/NOTIFY Kapasite ve Hata Senaryoları
535
- **Bilinen Riskler ve Mitigasyonlar:**
536
-
537
- | Risk Senaryosu | Etki | Mitigation (Zorunlu) |
538
- |---------------------------------------|-------------------------------------------|----------------------|
539
- | 1000+ concurrent panic / support vote | Connection pool tükenmesi, notify kaybı | Worker pool max 8, debounced bulk insert (max 500 kayıt/batch), 30sn içinde işlenemeyen kuyruk → DLQ + admin alert |
540
- | PostgreSQL restart / failover | Tüm LISTEN bağlantıları kopar | Tüm worker'lar otomatik reconnect + `LISTEN` yeniden abone olur (exponential backoff 1s → 60s) |
541
- | Unlogged tablo WAL eksikliği | Crash sonrası kuyruk kaybı | Kritik kuyruklar (panic, dispute) için **logged** fallback tablo + periyodik "replay from last known good" job |
542
- | Yüksek trafikte notify storm | CPU ve I/O şişmesi | `pg_notify` payload'ları 2KB ile sınırla; büyük payload'lar için sadece ID gönder + ayrı sorgu |
543
-
544
- - **Kapasite Testi Zorunluluğu:** Her belediye backend'i production'a alınmadan önce şu testleri geçmiş olmalıdır:
545
- - 5.000 satır/saniye `support_count_queue` insert + worker tüketimi
546
- - 200 panic sinyali/saniye debounced bulk upsert
547
- - 30 saniye PostgreSQL primary fail-over simülasyonu (pgbouncer + patroni)
548
-
549
- #### D5. Artifact, Deployment ve Güvenlik Güncellemeleri
550
- - Belediye backend production bundle'ları **code signing** (cosign veya sigstore) ile imzalanır.
551
- - Central Gateway, belediyeden gelen `/ready` ve heartbeat isteklerinde artifact imza hash'ini kontrol eder (opsiyonel, ileri seviye).
552
- - Güvenlik yamaları (Fastify, PostgreSQL client, crypto kütüphaneleri) **maksimum 14 gün** içinde tüm aktif belediyelere dağıtılmalıdır. Central "Security Patch Required" bayrağı koyabilir; bu bayrak açıkken belediye yeni iş/rapor kabul edemez (salt-okunur + mevcut iş tamamlama).
553
-
554
- **Sonuç:** Bu standartlar olmadan "kolay dağıtım" vaadi operasyonel borç yaratır. Central SaaS ekibi, her yeni belediye onboarding'inde bu maddelerin kontrol edildiğini teyit etmekle yükümlüdür.
555
-
556
- ---
557
-
558
- ## EK: Eksik Tablo Şemaları ve Ek Güvenlik Mimarisi
559
-
560
- ### 1. Ek Tablo Şemaları
561
-
562
- #### `ideas` ve `idea_votes` (Fikir Yönetimi)
563
- ```sql
564
- CREATE TABLE ideas (
565
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
566
- tenant_id UUID NOT NULL,
567
- citizen_id UUID NULL REFERENCES citizens(id), -- NULL = anonim (izin verilmez; daima hesap zorunlu)
568
- title VARCHAR(200) NOT NULL,
569
- description TEXT NOT NULL,
570
- status VARCHAR(20) NOT NULL DEFAULT 'pending_moderation'
571
- CHECK (status IN ('pending_moderation', 'active', 'rejected', 'implemented')),
572
- vote_score INTEGER NOT NULL DEFAULT 0, -- net puan: +1 ve -1 toplamı
573
- is_identity_verified BOOLEAN NOT NULL DEFAULT FALSE, -- fikir oluşturmak için TRUE zorunlu
574
- official_response TEXT NULL,
575
- category_id UUID NULL,
576
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
577
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
578
- );
579
-
580
- CREATE TABLE idea_votes (
581
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
582
- idea_id UUID NOT NULL REFERENCES ideas(id) ON DELETE CASCADE,
583
- citizen_id UUID NOT NULL REFERENCES citizens(id),
584
- direction SMALLINT NOT NULL CHECK (direction IN (1, -1)), -- +1 veya -1
585
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
586
- CONSTRAINT uq_idea_citizen_vote UNIQUE (idea_id, citizen_id) -- kişi başı bir oy
587
- );
588
- -- Oy değiştirme: mevcut satır güncellenir (direction UPDATE), yeni satır eklenmez.
589
- ```
590
-
591
- #### `gdpr_requests` (KVKK/GDPR Talep Yönetimi)
592
- ```sql
593
- CREATE TABLE gdpr_requests (
594
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
595
- tenant_id UUID NOT NULL,
596
- citizen_id UUID NOT NULL,
597
- request_type VARCHAR(30) NOT NULL
598
- CHECK (request_type IN ('account_deletion', 'data_export', 'anonymization')),
599
- status VARCHAR(20) NOT NULL DEFAULT 'pending'
600
- CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
601
- requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
602
- processed_at TIMESTAMPTZ NULL,
603
- anonymized_at TIMESTAMPTZ NULL,
604
- processed_by UUID NULL, -- muni_admin user_id
605
- notes TEXT NULL
606
- );
607
- CREATE INDEX idx_gdpr_citizen ON gdpr_requests(citizen_id);
608
- CREATE INDEX idx_gdpr_status ON gdpr_requests(status);
609
- ```
610
-
611
- #### `notification_dlq` (Başarısız Bildirim Dead Letter Queue)
612
- ```sql
613
- CREATE TABLE notification_dlq (
614
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
615
- tenant_id UUID NOT NULL,
616
- recipient_id UUID NOT NULL, -- citizen_id veya user_id
617
- channel VARCHAR(20) NOT NULL CHECK (channel IN ('email', 'sms', 'push')),
618
- payload JSONB NOT NULL, -- gönderilmek istenen içerik
619
- error_message TEXT,
620
- retry_count SMALLINT NOT NULL DEFAULT 0,
621
- last_attempt_at TIMESTAMPTZ,
622
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
623
- );
624
- CREATE INDEX idx_ndlq_tenant ON notification_dlq(tenant_id);
625
- ```
626
-
627
- #### `failed_telemetry_dlq` (Başarısız IoT Telemetri Dead Letter Queue)
628
- ```sql
629
- CREATE TABLE failed_telemetry_dlq (
630
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
631
- tenant_id UUID NOT NULL,
632
- device_id UUID NOT NULL,
633
- raw_payload JSONB NOT NULL, -- ham IoT JSON, parser crash olduğunda saklanır
634
- error_type VARCHAR(50), -- 'PARSER_CRASH', 'OUT_OF_BOUNDS', 'PAYLOAD_TOO_LARGE'
635
- error_detail TEXT,
636
- received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
637
- );
638
- CREATE INDEX idx_fdlq_device ON failed_telemetry_dlq(device_id);
639
- CREATE INDEX idx_fdlq_tenant ON failed_telemetry_dlq(tenant_id);
640
- ```
641
-
642
- ### 2. `public_map_cache` bbox_hash Üretim Kuralı
643
- ```sql
644
- CREATE UNLOGGED TABLE public_map_cache (
645
- tenant_id UUID NOT NULL,
646
- bbox_hash VARCHAR(64) NOT NULL, -- SHA-256 (normalize edilmiş bbox string'i)
647
- data JSONB NOT NULL,
648
- expires_at TIMESTAMPTZ NOT NULL,
649
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
650
- CONSTRAINT pk_pmc PRIMARY KEY (tenant_id, bbox_hash)
651
- );
652
- ```
653
- **bbox_hash Üretim Kuralı:** `bbox` parametresi `minLon,minLat,maxLon,maxLat` formatında gelir; her koordinat **6 ondalık basamağa** yuvarlanarak normalize edilir. Normalize string `SHA-256` ile hash'lenir. Örnek: `bbox=28.9785123456,41.0234567890,...` → normalize: `28.978512,41.023457,...` → SHA-256 hex. Bu sayede yakın bbox'ların birbirini es geçmesi (false miss) önlenir.
654
-
655
- ### 3. Kritik PostGIS GIST İndex'leri
656
- ```sql
657
- -- Belediye sınır poligonu sorguları (ST_Contains)
658
- CREATE INDEX idx_boundary_versions_geom ON municipal_boundary_versions
659
- USING GIST (boundaries);
660
- CREATE INDEX idx_boundary_active ON municipal_boundary_versions(municipality_id)
661
- WHERE active_to IS NULL; -- aktif sürümler için partial index
662
-
663
- -- İş emri konum aramaları
664
- CREATE INDEX idx_work_orders_location ON work_orders
665
- USING GIST (location);
666
-
667
- -- Panik sinyali konum aramaları
668
- CREATE INDEX idx_crisis_signals_coord ON crisis_signals
669
- USING GIST (coordinates);
670
- ```
671
-
672
- ### 4. Kimlik Doğrulama Güvenlik Mimarisi
673
-
674
- #### Refresh Token Rotation
675
- Her `POST /auth/refresh` çağrısında:
676
- 1. İstemci mevcut refresh token'ı gönderir.
677
- 2. Sunucu token'ı veritabanında kontrol eder (geçerli mi, tek kullanımlık mı).
678
- 3. Eski refresh token anında **geçersiz kılınır** ve silinir.
679
- 4. Yeni access token (15 dk) + yeni refresh token (7 gün / "beni hatırla" ile 30 gün) üretilip döndürülür.
680
- 5. **Çalınan token senaryosu:** Hacker eskimiş token'ı kullanmaya çalışırsa `401 TOKEN_REUSE_DETECTED` döner; oturum güvenlik tedbiri olarak tamamen sonlandırılır ve kullanıcıya "Şüpheli giriş tespit edildi" bildirimi gönderilir.
681
-
682
- #### HMAC-SHA256 Replay Koruması (Sistem İç İletişim)
683
- Central → Belediye komut kanalı ve `/ready` polling için:
684
- - `x-kentim-timestamp`: Unix milisaniye zaman damgası (zorunlu)
685
- - `x-kentim-nonce`: UUIDv4 (zorunlu)
686
- - `x-kentim-signature`: `HMAC-SHA256(HMAC_MASTER_KEY, method + path + timestamp + nonce + body_sha256)`
687
- - **Geçerlilik Penceresi:** Sunucu zamanından ±5 dakika; dışarısındaki istekler `401` ile reddedilir.
688
- - **Nonce Koruması:** Her nonce, **PostgreSQL'deki `hmac_nonces` tablosunda** (kısmi indeks: `WHERE used = FALSE`) 10 dakika TTL ile saklanır; aynı nonce ikinci kez gelirse `401 REPLAY_DETECTED` döner. Tablo periyodik olarak temizlenir (`DELETE FROM hmac_nonces WHERE created_at < NOW() - INTERVAL '10 minutes'`).
689
-
690
- ---
691
-
692
- ### 5. Yeni Eksik Veri Şemaları DDL Tanımları (S1-S7)
693
-
694
- #### `work_orders` Tablosu Şeması (S1 Düzeltmesi)
695
- ```sql
696
- CREATE TABLE work_orders (
697
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
698
- tenant_id VARCHAR(50) NOT NULL,
699
- tracking_code VARCHAR(30) UNIQUE NOT NULL, -- Format: KENT-[plaka]-XXXXXXXX
700
- title VARCHAR(150) NOT NULL,
701
- description TEXT NOT NULL,
702
- status VARCHAR(30) NOT NULL DEFAULT 'pending'
703
- CHECK (status IN (
704
- 'pending', 'moderation', 'dispatched_to_department',
705
- 'assigned_to_team', 'assigned_to_worker', 'in_progress',
706
- 'pending_chief_approval', 'pending_manager_review',
707
- 'resolved', 'rejected', 'reopened', 'chronic_archived'
708
- )),
709
- -- chronic_archived:
710
- -- Vatandaş bir raporu maksimum 2 kez reopen ettikten sonra yeni rapor açtığında
711
- -- eski rapor bu duruma alınır. is_public_visible = false yapılır, SLA durdurulur.
712
- -- Yeni rapor parent_reopened_work_order_id ile eski rapora bağlanır.
713
- -- Bu, kronik sorun zincirlerini admin panelinde izlenebilir hale getirir.
714
- priority VARCHAR(10) NOT NULL DEFAULT 'normal'
715
- CHECK (priority IN ('low', 'normal', 'high', 'urgent', 'crisis')),
716
- source_type VARCHAR(20) NOT NULL DEFAULT 'citizen_report'
717
- CHECK (source_type IN ('citizen_report', 'manual_internal', 'integration', 'scheduled')),
718
- -- Reopen yönetimi
719
- reopen_count SMALLINT NOT NULL DEFAULT 0, -- Max 2; bu sınıra ulaştığında reopen butonu kilitlenir
720
- reopen_reason TEXT NULL,
721
- reopen_notes TEXT NULL,
722
- -- Re-dispatch ve atama yönetimi
723
- re_dispatch_count SMALLINT NOT NULL DEFAULT 0, -- Sadece müdür/admin re-dispatch'leri sayılır
724
- is_force_assigned BOOLEAN NOT NULL DEFAULT FALSE, -- Admin anlaşmazlık kuyruğundan kesin atama yaptığında TRUE
725
- is_manager_bypassed BOOLEAN NOT NULL DEFAULT FALSE, -- Müdür şefi bypass edip doğrudan personele atadığında TRUE
726
- -- Geofence
727
- geofence_bypass_requested BOOLEAN NOT NULL DEFAULT FALSE,
728
- geofence_bypass_approved BOOLEAN NOT NULL DEFAULT FALSE,
729
- -- Fotoğraf ret devre kesici (circuit breaker)
730
- photo_rejection_count SMALLINT NOT NULL DEFAULT 0, -- 3'e ulaştığında pending_manager_review'a eskalasyon tetiklenir; reassign_worker ile sıfırlanır
731
- -- Destek oyu sayacı (debounced worker ile güncellenir)
732
- support_count INTEGER NOT NULL DEFAULT 0,
733
- -- Vatandaş geri bildirimi
734
- satisfaction_stars SMALLINT NULL CHECK (satisfaction_stars BETWEEN 1 AND 5),
735
- -- Atama zinciri
736
- assigned_worker_id UUID NULL,
737
- approval_authority_id UUID NULL, -- Onay yetkisini kimin alacağı (şef veya müdür)
738
- -- Coğrafi bağlantılar
739
- boundary_version_id UUID NULL REFERENCES municipal_boundary_versions(id),
740
- parent_reopened_work_order_id UUID NULL REFERENCES work_orders(id) ON DELETE SET NULL,
741
- location GEOMETRY(Point, 4326) NOT NULL,
742
- -- Zaman damgaları
743
- sla_started_at TIMESTAMPTZ NULL, -- dispatched_to_department anında set edilir
744
- sla_deadline_at TIMESTAMPTZ NULL, -- Hesaplanmış SLA bitiş zamanı
745
- resolved_at TIMESTAMPTZ NULL,
746
- is_sla_breached BOOLEAN NOT NULL DEFAULT FALSE,
747
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
748
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
749
- );
750
- CREATE INDEX idx_work_orders_tenant ON work_orders(tenant_id);
751
- CREATE INDEX idx_work_orders_tracking ON work_orders(tracking_code);
752
- CREATE INDEX idx_work_orders_status ON work_orders(status);
753
- CREATE INDEX idx_work_orders_priority ON work_orders(priority);
754
- CREATE INDEX idx_work_orders_chronic ON work_orders(parent_reopened_work_order_id) WHERE status = 'chronic_archived';
755
- ```
756
-
757
- #### `citizens` Tablosu Şeması (S2 Düzeltmesi)
758
- ```sql
759
- CREATE TABLE citizens (
760
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
761
- email VARCHAR(255) UNIQUE NOT NULL,
762
- email_verified BOOLEAN NOT NULL DEFAULT FALSE,
763
- email_verified_at TIMESTAMPTZ NULL,
764
- is_identity_verified BOOLEAN NOT NULL DEFAULT FALSE,
765
- identity_verified_at TIMESTAMPTZ NULL,
766
- display_name VARCHAR(100) NOT NULL,
767
- status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'passive', 'anonymized')),
768
- kvkk_accepted_at TIMESTAMPTZ NOT NULL,
769
- notification_email BOOLEAN DEFAULT TRUE,
770
- notification_push BOOLEAN DEFAULT TRUE,
771
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
772
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
773
- );
774
- CREATE INDEX idx_citizens_email ON citizens(email);
775
- ```
776
-
777
- #### `crisis_signals` Tablosu Şeması (S3 Düzeltmesi)
778
- ```sql
779
- CREATE TABLE crisis_signals (
780
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
781
- tenant_id UUID NOT NULL,
782
- device_id VARCHAR(255) NOT NULL,
783
- citizen_id VARCHAR(255) NOT NULL DEFAULT 'anonymous',
784
- coordinates GEOMETRY(Point, 4326) NOT NULL,
785
- received_at TIMESTAMPTZ NOT NULL,
786
- is_closed BOOLEAN NOT NULL DEFAULT FALSE,
787
- closed_at TIMESTAMPTZ NULL,
788
- CONSTRAINT uq_device UNIQUE (device_id)
789
- );
790
- CREATE INDEX idx_crisis_signals_coord ON crisis_signals USING GIST (coordinates);
791
- CREATE INDEX idx_crisis_signals_tenant ON crisis_signals(tenant_id);
792
- ```
793
-
794
- #### `central_health_logs` Tablosu Şeması (S4 Düzeltmesi)
795
- ```sql
796
- CREATE TABLE central_health_logs (
797
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
798
- municipality_id UUID NOT NULL,
799
- status VARCHAR(20) NOT NULL CHECK (status IN ('UP', 'DEGRADED', 'DOWN')),
800
- latency_ms INTEGER NULL,
801
- db_connected BOOLEAN NULL,
802
- -- redis_connected kaldırıldı: Redis kullanılmamaktadır.
803
- disk_usage_percentage DECIMAL(5,2) NULL,
804
- pg_listener_active BOOLEAN NULL, -- LISTEN/NOTIFY worker'larının aktif olup olmadığı
805
- error_message TEXT NULL,
806
- checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
807
- );
808
- CREATE INDEX idx_health_logs_muni ON central_health_logs(municipality_id, checked_at DESC);
809
- ```
810
-
811
- #### `central_analytics_metadata` Tablosu Şeması (S5 Düzeltmesi)
812
- ```sql
813
- CREATE TABLE central_analytics_metadata (
814
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
815
- municipality_id UUID NOT NULL,
816
- month DATE NOT NULL, -- Ay başlangıcı (YYYY-MM-01)
817
- total_work_orders INTEGER DEFAULT 0,
818
- sla_compliance_rate DECIMAL(5,2) DEFAULT 0,
819
- avg_resolution_time_minutes INTEGER DEFAULT 0,
820
- citizen_satisfaction_stars DECIMAL(3,2) DEFAULT 0,
821
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
822
- CONSTRAINT uq_muni_month UNIQUE (municipality_id, month)
823
- );
824
- ```
825
-
826
- #### `dispute_logs` Tablosu Şeması (S6 Düzeltmesi)
827
- ```sql
828
- CREATE TABLE dispute_logs (
829
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
830
- tenant_id UUID NOT NULL,
831
- work_order_id UUID NOT NULL REFERENCES work_orders(id),
832
- offline_worker_id UUID NOT NULL,
833
- online_worker_id UUID NULL,
834
- local_completed_at TIMESTAMPTZ NOT NULL, -- Çevrimdışı personelin yerel saati
835
- server_handover_at TIMESTAMPTZ NULL, -- Auto-handover zamanı
836
- conflict_type VARCHAR(30) NOT NULL CHECK (conflict_type IN ('AUTO_HANDOVER', 'REASSIGNED', 'DOUBLE_COMPLETE')),
837
- status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'approved_override')),
838
- resolved_by UUID NULL,
839
- resolved_at TIMESTAMPTZ NULL,
840
- notes TEXT NULL,
841
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
842
- );
843
- CREATE INDEX idx_dispute_work_order ON dispute_logs(work_order_id);
844
- CREATE INDEX idx_dispute_status ON dispute_logs(status, tenant_id);
845
- ```
846
-
847
- #### `integration_logs` Tablosu Şeması (S7 Düzeltmesi)
848
- ```sql
849
- CREATE TABLE integration_logs (
850
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
851
- tenant_id UUID NOT NULL,
852
- device_id UUID NOT NULL,
853
- event_type VARCHAR(30) NOT NULL CHECK (event_type IN ('SUCCESS', 'FAILED', 'OUT_OF_BOUNDS', 'THRESHOLD_HIT', 'DUPLICATE_SKIPPED')),
854
- raw_payload JSONB NULL,
855
- error_detail TEXT NULL,
856
- work_order_id UUID NULL, -- Tetiklenen iş emri (varsa)
857
- received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
858
- );
859
- CREATE INDEX idx_integration_logs_device ON integration_logs(device_id, received_at DESC);
860
- ```
861
-
862
- #### `municipalities` Tablosu Şeması (S8 Düzeltmesi)
863
- ```sql
864
- CREATE TABLE municipalities (
865
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
866
- name VARCHAR(150) NOT NULL UNIQUE,
867
- city_slug VARCHAR(100) NOT NULL,
868
- status VARCHAR(20) NOT NULL DEFAULT 'pending_approval' CHECK (status IN ('pending_approval', 'active', 'passive', 'suspended')),
869
- license_expires_at TIMESTAMPTZ NOT NULL,
870
- grace_period_ends_at TIMESTAMPTZ NULL,
871
- api_key_hash VARCHAR(64) NOT NULL, -- Rotasyon ve güvenlik için sha256 hash
872
- hmac_master_key VARCHAR(128) NOT NULL, -- Loopback için şifreli anahtar
873
- priority_order INTEGER NOT NULL DEFAULT 1, -- Çakışma durumundaki fallback sırası
874
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
875
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
876
- );
877
- CREATE INDEX idx_municipalities_slug ON municipalities(city_slug);
878
- CREATE INDEX idx_municipalities_status ON municipalities(status);
879
- ```
880
-
881
- #### PostGIS Centroid Bazlı En Yakın Belediye Sorgu Modellemesi (Afet Panik Yönlendirme)
882
- Afet modunda coğrafi sınır dışındaki veya çakışan panik sinyallerinin en yakın aktif belediyeye atanabilmesi için veritabanında çalışacak PostGIS mesafe sorgusu:
883
- ```sql
884
- -- Panik koordinatına en yakın aktif belediyeyi bulma
885
- SELECT m.id AS municipality_id, m.name
886
- FROM municipalities m
887
- JOIN municipal_boundary_versions mbv ON m.id = mbv.municipality_id
888
- WHERE m.status = 'active'
889
- AND mbv.active_to IS NULL
890
- ORDER BY mbv.boundaries <-> ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)
891
- LIMIT 1;
892
- ```
893
- *(Not: `<->` operatörü, PostGIS GIST indeksi üzerinden 3B/2B bounding box centroid'lerine göre en yakın komşuluğu (KNN) O(log N) karmaşıklığında bularak veritabanını kilitlemeden en hızlı yanıtı üretir.)*
894
-
895
- #### Çevrimdışı Çakışma Çözümü Çift Hak Ediş Kontrolü (Double Payout Alert Trigger)
896
- Bir iş emri hem canlı sistemde çözülüp hak ediş puanı yazılmışsa hem de `dispute_logs` üzerinden onaylanmak isteniyorsa, amire uyarı üretilmesini sağlayan veritabanı trigger tasarımı:
897
- ```sql
898
- CREATE OR REPLACE FUNCTION check_double_payout_conflict()
899
- RETURNS TRIGGER AS $$
900
- DECLARE
901
- v_already_payout BOOLEAN;
902
- BEGIN
903
- -- Ana iş emrinde resolved aşamasında başka bir personele puan yazılmış mı kontrol et
904
- SELECT EXISTS(
905
- SELECT 1 FROM work_orders
906
- WHERE id = NEW.work_order_id
907
- AND status = 'resolved'
908
- AND assigned_worker_id IS NOT NULL
909
- AND assigned_worker_id != NEW.offline_worker_id
910
- ) INTO v_already_payout;
911
-
912
- -- Eğer daha önce başka bir personele puan yazılmışsa ve amir onay veriyorsa
913
- -- statüyü otomatik 'approved_override' olarak işaretle (amir onayı kayıt altına alınır)
914
- IF v_already_payout AND NEW.status = 'approved' THEN
915
- NEW.status := 'approved_override';
916
- END IF;
917
-
918
- RETURN NEW;
919
- END;
920
- $$ LANGUAGE plpgsql;
921
-
922
- CREATE TRIGGER trg_dispute_double_payout
923
- BEFORE UPDATE ON dispute_logs
924
- FOR EACH ROW
925
- EXECUTE FUNCTION check_double_payout_conflict();
926
- ```