# Dashboard Widget Sistemi — Tasarım Spesifikasyonu

**Tarih:** 2026-03-20
**Revizyon:** 3 (review bulguları ile kapsamlı güncelleme)
**Kapsam:** HMI Pano personel ana sayfası için tam dinamik, DB+TS hibrit, WebSocket destekli, olay takipli, responsive, tamamen özelleştirilebilir dashboard widget sistemi.

---

## 1. Genel Bakış

Widget sistemi mevcut ISablon + SablonRenderer altyapısını yeniden kullanarak, her widget'ı ve item'ını tamamen JSON-driven, sıfırdan tasarlanabilir kılar. Sabit tip listesi yoktur — her widget custom JSON şablondur.

### Hibrit Kaynak Modeli

| Kaynak | Ne saklar | Ne zaman değişir |
|--------|----------|------------------|
| **TS (kod)** | Hazır widget tanımları, sistem widget'ları, fabrika varsayılanları | Kod güncellendiğinde |
| **DB (JSON)** | Kullanıcı özelleştirmeleri, custom widget'lar, layout override | Runtime'da, kullanıcı kaydettiğinde |

Birleştirme: `TS varsayılanlar + DB override/eklentiler = Final dashboard`
- TS'te tanımlı, DB'de override varsa → DB kazanır
- TS'te tanımlı, DB'de yoksa → TS olduğu gibi
- DB'de yeni, TS'te yoksa → eklenir

### Sahiplik Modeli

Dashboard cihaz bazlı saklanır (`sahip: cihazId`). HMI kiosk'ta tüm personeller aynı layout'u görür. Personel değiştiğinde (kart okutma) widget verileri yenilenir ama layout değişmez. İleride personel bazlı override eklenebilir (`sahip: personelId`, merge sırası: personel > cihaz > TS).

---

## 2. Veri Modeli

### IWidget

```typescript
interface IWidget {
  id: string;
  baslik?: string;
  simge?: string;
  aciklama?: string;

  // Grid pozisyon
  pozisyon: {
    genislik: number;   // grid span
    yukseklik: number;  // grid span
    x?: number;         // sadece serbest modda piksel
    y?: number;         // sadece serbest modda piksel
    sira?: number;      // grid modda auto-flow sırası (CSS order)
  };

  // Görünüm — ISablon reuse (kart tasarım sistemi)
  // sablon tanımlanırsa mod, arkaPlan, bolgeler, elementler ZORUNLU
  // Tanımlanmazsa stil alanı fallback olarak kullanılır
  sablon?: ISablon;
  // Veya basit stil (sablon yoksa fallback)
  stil?: {
    arkaPlan?: string;
    gradyan?: string;
    renkYazi?: string;
    borderRadius?: number;
    animasyon?: string;
    golge?: string;
  };

  // Veri kaynağı
  veriKaynagi?: IWidgetVeriKaynagi;

  // İçindeki item'lar — her biri custom JSON
  itemler?: IWidgetItem[];

  // Olaylar — JS fonksiyon stringleri
  olaylar?: {
    tiklanma?: string;
    yenilendi?: string;
    esikAsimi?: string;
    ciftTiklama?: string;
    [key: string]: string | undefined;
  };

  // Meta
  kaynak: 'kod' | 'db';
  gorunur?: boolean;
  silindi?: boolean;           // soft delete — yukle() sorgusunda { silindi: { $ne: true } }
  yetki?: string[];
  sira?: number;
}

interface IWidgetVeriKaynagi {
  mod: 'aggregate' | 'sabit' | 'soket' | 'hesaplama';
  // aggregate: MongoDB aggregate pipeline çalıştır
  // sabit: veriKaynagi.sabitVeri kullan (statik JSON)
  // soket: Socket.IO event dinle
  // hesaplama: piFunc ile diğer widget verilerinden hesapla
  vt?: string;                       // pipeline tanımlıysa IGNORED; pipeline yoksa otomatik { $match: { vt } } eklenir
  pipeline?: any[];                  // MongoDB aggregate (sabit pipeline)
  sabitVeri?: any;                   // mod:'sabit' için statik veri
  soketOlay?: string;                // Socket.IO event adı
  soketFiltre?: Record<string, any>; // Gelen event payload filtresi
  yenilemeAraligi?: number;          // saniye (0 = sadece soket, varsayılan 60)
  // Dinamik pipeline/dönüşüm fonksiyonları
  // Çalıştırma: new Function('veri', 'widget', fnString)
  // Dönüş: pipeline → any[], donusum → any
  // Hata yönetimi: her ikisi de try-catch ile sarmalanır,
  //   hata durumunda widget'ın hata() signal'ı set edilir
  piFunc?: string;
  donusumFunc?: string;
}

interface IWidgetItem {
  id: string;
  sablon?: ISablon;
  veriKaynagi?: IWidgetVeriKaynagi;
  olaylar?: Record<string, string>;
  [key: string]: any;                // Tamamen custom JSON alanlar
}
```

### DB Yapısı

```
hareketler koleksiyonu:
{
  _id: ObjectId,
  vt: 'FikoYT.sistem.dashboard',
  sahip: cihazId,                    // localStorage'dan HW UUID
  veri: {
    layout: 'grid' | 'serbest',
    kolonSayisi: 4,
    aralik: 12,
    widgetler: IWidget[]
  }
}
```

---

## 3. Responsive Grid Layout

```
Masaüstü (>1200px): 4 sütun
Tablet (768-1200px): 3 sütun
HMI Panel (600-768px): 2 sütun
Mobil (<600px): 1 sütun

Widget boyutları grid biriminde (genislik x yukseklik):
1x1 = küçük kart (saat, sayaç)
2x1 = geniş kart (trend çizgisi)
2x2 = büyük kart (tablo, grafik)
4x1 = tam genişlik bar (duyuru)

Grid modu: CSS Grid auto-flow, widget genislik/yukseklik → grid-column/grid-row span
Serbest mod: position absolute, x/y piksel (sablon editörü gibi)
```

---

## 4. WebSocket Entegrasyonu

```
// Her widget kendi soketOlay'ını dinleyebilir
veriKaynagi: {
  mod: 'soket',
  soketOlay: 'FikoYT.sistem.io.cihaz.guncellendi',
  soketFiltre: { tip: 'uretim_sayac' },
  yenilemeAraligi: 30   // fallback polling (soket yoksa)
}

// Mevcut Socket.IO event'leri reuse:
FikoYT.sistem.io.cihaz.guncellendi   → Makine durumları
FikoYT.sistem.io.cihaz.baglandi      → Cihaz online/offline
FikoYT.sistem.io.cihaz.telemetri     → Sensör verileri
FikoYT.sistem.io.mail.yeni           → Yeni mail sayısı

// Lifecycle:
// - WidgetRenderer.ngOnInit → dashboardService.soketDinle(widget, callback) çağırır
// - WidgetRenderer.ngOnDestroy → subscription unsubscribe
// - DashboardService.temizle() tüm soketAbonelikler'i kapatır + destroy$.next()
// - dashboard.component.ngOnDestroy → temizle() çağırır
// - yenilemeAraligi: 0 → polling kurulmaz, sadece soket dinlenir
// - takeUntil(destroy$) pattern kullanılır
```

---

## 5. Olay Sistemi

```typescript
// Olay fonksiyonu oluşturma:
// new Function('veri', 'widget', 'router', 'bildirim', 'guncelle', olayKodu)
// Try-catch ile sarmalanır, hata console.warn ile loglanır

olaylar: {
  tiklanma: `router.navigate(['/🧩/📟/pano/personel/uretim'])`,
  esikAsimi: `
    if (veri.toplam > 500) {
      guncelle({ stil: { arkaPlan: '#f6ffed' } });
      bildirim('Hedef aşıldı!', 'basarili');
    }
  `,
  yenilendi: `
    guncelle({
      stil: {
        gradyan: veri.yuzde > 80
          ? 'linear-gradient(135deg, #1b5e20, #43a047)'
          : 'linear-gradient(135deg, #b71c1c, #e53935)'
      }
    });
  `,
}

// Runtime context parametreleri:
// veri      — widget'ın son çekilen verisi (any)
// widget    — IWidget referansı (READ-ONLY, doğrudan mutasyon YAPILMAZ)
// router    — Angular Router instance (inject ile alınır)
// bildirim  — (mesaj: string, tip: 'basarili'|'uyari'|'hata') => void
//              nzNotification wrapper
// guncelle  — (degisiklikler: Partial<IWidget>) => void
//              dashboardService.widgetGuncelle(id, degisiklikler) çağırır
//              Immutable güncelleme — widget referansını değil, servis state'ini günceller

// Hata yönetimi:
// try { fn(veri, widget, router, bildirim, guncelle); }
// catch(e) { console.warn('Widget olay hatası:', widget.id, e); }
```

---

## 6. ISablon Reuse — Widget Tasarımı

Mevcut kart tasarım sistemi (`ISablon` + `SablonRenderer`) widget render için de kullanılır:

- Her widget'ın `sablon` alanı = kart tasarım editöründeki aynı ISablon
- `sablon?` tanımlanırsa `mod`, `arkaPlan`, `bolgeler`, `elementler` ZORUNLU. Tanımlanmazsa `stil` alanı fallback
- Temel elementler: `sabit-metin`, `parametre-render`, `sekil`, `resim`
- `parametre-render` widget verisinden binding yapar ({{toplam}}, {{yuzde}})
- SablonRenderer input'u: `[parametreDegerleri]` (not: `parametreler` değil!)
- Sablon editörü widget editörü olarak da çalışır
- Animasyonlar: giriş, hover, sürekli (pulse, glow, float)
- Sablon yoksa `stil` alanından basit card render (fallback)

### Item Tasarımı

Her item'ın kendi `sablon`'u olabilir:
```json
{
  "id": "item-1",
  "sablon": { "mod": "serbest", "arkaPlan": {"tur":"renk","deger":"#1a1a2e"}, "bolgeler": [], "elementler": [...] },
  "veriKaynagi": { "mod": "aggregate", "pipeline": [...] }
}
```

### Widget Özel Element Türleri

ISablon'daki temel element türlerine ek olarak dashboard widget'larında kullanılabilecek zengin türler:

#### Sayısal & İlerleme

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `sayac` | Büyük sayı + trend oku (↑↓) + delta yüzdesi + renk eşiği | `▲ 450` yeşil, `▼ 12` kırmızı |
| `ilerleme-cubugu` | Linear progress bar — doluluk yüzdesi + etiket | `████░░ %74` |
| `ilerleme-halka` | Circular/ring progress — SVG çember + merkez değer | Donut ring |
| `gauge` | Yarım daire gösterge (0-100) — ibre + renk zonları | Hız göstergesi tarzı |
| `seviye-cubugu` | Dikey bar seviye göstergesi — tank doluluk gibi | Sıvı seviye |

#### Durum & Gösterge

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `durum-dot` | Renkli durum noktası + label + nabız animasyonu | 🟢 Çalışıyor |
| `durum-kart` | Mini durum kartı — simge + başlık + açıklama + renk şeridi | Kompakt bilgi kartı |
| `sinyal-cubugu` | Wi-Fi sinyal çubukları tarzı — 1-5 seviye | ▂▄▆█ |
| `led-gosterge` | LED ışık göstergesi — açık/kapalı/yanıp sönen | 🔴🟢🟡 |
| `trafik-isigi` | 3'lü trafik ışığı — kırmızı/sarı/yeşil | ⚫🟡⚫ |

#### Grafik & Veri Görselleştirme

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `sparkline` | Mini SVG çizgi grafik — son N veri noktası, gradient dolgu | Küçük trend çizgisi |
| `bar-grafik` | Mini SVG bar chart — dikey çubuklar + etiketler | ▂▅▇▃▆ |
| `pasta-grafik` | SVG donut/pie chart — veri dizisinden segmentler + legend | Pasta dilimi |
| `alan-grafik` | SVG area chart — çizgi + gradient altı dolgu | Dağ grafiği |
| `radar-grafik` | SVG radar/spider chart — çok boyutlu karşılaştırma | Örümcek ağı |
| `isı-haritasi` | Grid bazlı ısı haritası — hücre renkleri veri yoğunluğuna göre | Haftalık aktivite grid'i |

#### Saat & Zaman

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `dijital-saat` | Dijital saat — HH:MM:SS + tarih + timezone | `14:35:22` |
| `analog-saat` | SVG analog saat — akrep/yelkovan/saniye + kadran | Klasik duvar saati |
| `countdown` | Hedef tarihe geri sayım — gün:saat:dakika:saniye | `02:14:35:22` |
| `kronometr` | Başlat/durdur kronometresi — lap time destekli | `00:05:32.4` |
| `zaman-cizelgesi` | Yatay timeline — olaylar kronolojik sırada | ●──●──●──● |

#### Liste & Tablo

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `liste-tekrar` | Veri dizisinden tekrarlayan element grubu — her item kendi alt elementlerine sahip | Kart listesi |
| `sıralama-listesi` | Sıralı liste + sıra numarası + bar göstergesi | #1 Tezgah A — 450 |
| `mini-tablo` | Kompakt tablo — sütun başlıkları + satırlar + zebra stili | Mini data grid |
| `kanban-sutun` | Kanban sütunu — durum başlığı + kart sayısı + mini kartlar | Trello sütunu |

#### Medya & Dekoratif

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `avatar` | Yuvarlak avatar — resim/harf/simge + durum badge | 👤🟢 |
| `badge` | Bildirim badge — sayı + renk + nabız animasyonu | 🔴 5 |
| `ticker` | Yatay kayan metin — duyuru bandı | ← Üretim hedefi: 500 adet → |
| `qr-kod` | QR kod üretici — dinamik veri binding | QR kare |
| `hava-durumu` | Hava durumu ikonu + sıcaklık (API veya sabit) | ☀️ 28°C |
| `lottie` | Lottie JSON animasyonu — custom animasyonlar | Animasyon player |
| `iframe` | Gömülü sayfa — URL binding + sandbox | Mini web sayfası |
| `markdown` | Markdown render — duyuru/not gösterimi | Zengin metin |

#### Etkileşimli

| Element Türü | Açıklama | Görsel |
|-------------|----------|--------|
| `buton` | Tıklanabilir buton — olay tetikleme + stil + simge | [▶ Başlat] |
| `toggle` | Açma/kapama anahtarı — durum değişimi + olay | ○━━● |
| `slider` | Kaydırma çubuğu — değer ayarlama + min/max | ──●────── |
| `tab-grup` | Sekme grubu — farklı içerik setleri arasında geçiş | [A] [B] [C] |

---

## 7. Hazır Widget Tanımları (TS)

```typescript
// src/app/uygulamalar/hmi-pano/dashboard/hazir-widgetler.ts

export const VARSAYILAN_WIDGETLER: IWidget[] = [
  // Saat
  { id: 'sys-saat', baslik: 'Saat', simge: '⏰',
    pozisyon: { genislik: 1, yukseklik: 1 },
    sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] },
    kaynak: 'kod' },

  // Bugün Üretim
  { id: 'sys-uretim-sayac', baslik: 'Bugün Üretim', simge: '📊',
    pozisyon: { genislik: 1, yukseklik: 1 },
    veriKaynagi: {
      mod: 'aggregate',
      pipeline: [
        { $match: { uygulama: 'FikoYT.sistem.hmi.uretim', 'veri.icerikler.ek._secilenSetId': 'hmi.uretim-sayac', silindi: { $ne: true } } },
        { $count: 'toplam' }
      ],
      soketOlay: 'FikoYT.sistem.io.cihaz.guncellendi',
      yenilemeAraligi: 30
    },
    sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] },
    kaynak: 'kod' },

  // Aktif İş Emirleri (liste widget)
  { id: 'sys-is-emirleri', baslik: 'Aktif İş Emirleri', simge: '📋',
    pozisyon: { genislik: 2, yukseklik: 2 },
    veriKaynagi: { mod: 'aggregate', pipeline: [...], yenilemeAraligi: 60 },
    itemler: [
      { id: 'ie-item', sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] } }
    ],
    kaynak: 'kod' },

  // Tezgah Durumları (soket widget)
  { id: 'sys-tezgah', baslik: 'Tezgah Durumları', simge: '🏭',
    pozisyon: { genislik: 2, yukseklik: 1 },
    veriKaynagi: { mod: 'soket', soketOlay: 'FikoYT.sistem.io.cihaz.telemetri', yenilemeAraligi: 10 },
    sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] },
    kaynak: 'kod' },

  // Bekleyen Bakım Talepleri
  { id: 'sys-bakim', baslik: 'Bekleyen Bakım', simge: '🔧',
    pozisyon: { genislik: 1, yukseklik: 1 },
    veriKaynagi: { mod: 'aggregate', pipeline: [...] },
    olaylar: {
      esikAsimi: `if (veri.toplam > 3) { guncelle({ stil: { gradyan: 'linear-gradient(135deg, #b71c1c, #e53935)' } }); bildirim(veri.toplam + ' bekleyen bakım!', 'uyari'); }`
    },
    kaynak: 'kod' },

  // Uygulama Kartları (mevcut ana sayfa kartları)
  { id: 'sys-uygulamalar', baslik: 'Uygulamalar', simge: '📱',
    pozisyon: { genislik: 4, yukseklik: 1 },
    veriKaynagi: { mod: 'sabit' },
    itemler: [
      { id: 'app-projeler', baslik: 'Projeler', simge: '📁', route: 'projeler', renk: '#fa8c16' },
      { id: 'app-uretim', baslik: 'Üretim', simge: '⚙️', route: 'uretim', renk: '#f5222d' },
      { id: 'app-satis', baslik: 'Satış', simge: '🛒', route: 'satis', renk: '#ff9800' },
      { id: 'app-yonetim', baslik: 'Yönetim', simge: '🏢', route: 'yonetim-kayit', renk: '#722ed1' },
      { id: 'app-posta', baslik: 'Posta', simge: '📧', route: 'posta', renk: '#1890ff' },
    ],
    sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] },
    kaynak: 'kod' },

  // Duyuru Panosu
  { id: 'sys-duyuru', baslik: 'Duyuru', simge: '📢',
    pozisyon: { genislik: 4, yukseklik: 1 },
    veriKaynagi: { mod: 'sabit', sabitVeri: { metin: 'Hoş geldiniz' } },
    sablon: { mod: 'serbest', arkaPlan: {...}, bolgeler: [], elementler: [...] },
    kaynak: 'kod' },
];
```

---

## 8. Component Yapısı

```
src/app/uygulamalar/hmi-pano/dashboard/
├── interfaces/
│   └── widget.interface.ts              — IWidget, IWidgetItem, IWidgetVeriKaynagi
├── hazir-widgetler.ts                   — VARSAYILAN_WIDGETLER (TS tanımları)
├── dashboard.service.ts                 — DB CRUD + TS merge + veri çekme + soket + cache
├── dashboard.component.ts              — CSS Grid + CDK DragDrop + düzenleme modu
├── dashboard.component.html
├── dashboard.component.less
├── widget-renderer/
│   ├── widget-renderer.component.ts     — Tek widget render (sablon + veri + olaylar)
│   ├── widget-renderer.component.html
│   └── widget-renderer.component.less
└── widget-editor/
    ├── widget-editor.component.ts       — Widget düzenleme drawer (NzDrawer)
    ├── widget-editor.component.html
    └── widget-editor.component.less
```

NOT: `widget-veri.service.ts` kaldırıldı — tüm veri logic'i `dashboard.service.ts` içinde (tek sorumluluk, servis çoğaltma yok).

---

## 9. Dashboard Service

```typescript
@Injectable({ providedIn: 'root' })
export class DashboardService {
  private kodWidgetler = VARSAYILAN_WIDGETLER;
  dbWidgetler = signal<IWidget[]>([]);
  tumWidgetler = computed(() => this.birlestir());
  private soketAbonelikler = new Map<string, Subscription>();
  private destroy$ = new Subject<void>();

  // Cihaz bazlı yükle
  // cihazId kaynağı: dashboard.component.ngOnInit → localStorage.getItem('fikoyt_cihaz_kimlik')
  async yukle(cihazId: string): Promise<void> { ... }
  async kaydet(cihazId: string, widgetler: IWidget[]): Promise<void> { ... }

  private birlestir(): IWidget[] {
    const db = this.dbWidgetler();
    const dbIds = new Set(db.map(w => w.id));
    return [
      ...this.kodWidgetler.filter(kw => !dbIds.has(kw.id)).map(kw => ({ ...kw })),
      ...db
    ].filter(w => w.gorunur !== false)
     .sort((a, b) => (a.sira ?? 0) - (b.sira ?? 0));
  }

  // Widget verisi çek
  async widgetVerisiCek(widget: IWidget): Promise<any> { ... }

  // Socket.IO dinleme (takeUntil ile cleanup)
  soketDinle(widget: IWidget, callback: (veri: any) => void): void { ... }

  // Tüm soket aboneliklerini temizle
  temizle(): void {
    this.soketAbonelikler.forEach(s => s.unsubscribe());
    this.soketAbonelikler.clear();
    this.destroy$.next();
  }

  // Personel değiştiğinde veri yenile (layout değişmez)
  personelDegisti(): void {
    // Tüm widget verilerini yeniden çek
    // soket aboneliklerini resetle
  }

  // CRUD
  widgetEkle(widget: IWidget): void { ... }
  widgetGuncelle(id: string, degisiklikler: Partial<IWidget>): void { ... }
  widgetSil(id: string): void { ... }
  layoutSifirla(): void { this.dbWidgetler.set([]); }
}
```

---

## 10. Widget Renderer

```typescript
@Component({
  selector: 'widget-renderer',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="widget-kapsayici" [class.duzenleme]="duzenleModu()"
         [style.grid-column]="'span ' + widget().pozisyon.genislik"
         [style.grid-row]="'span ' + widget().pozisyon.yukseklik"
         [style.order]="widget().pozisyon.sira ?? 0"
         (click)="olayTetikle('tiklanma')"
         (dblclick)="olayTetikle('ciftTiklama')">

      <!-- Başlık çubuğu -->
      <div class="widget-baslik">
        <span class="widget-simge">{{ widget().simge }}</span>
        <span class="widget-baslik-metin">{{ widget().baslik }}</span>
        @if (duzenleModu()) {
          <button (click)="$event.stopPropagation(); duzenle()">⚙️</button>
          <button (click)="$event.stopPropagation(); sil()">✕</button>
        }
        @if (yukleniyor()) { <nz-spin nzSimple nzSize="small" /> }
        @if (hata()) { <span class="widget-hata" nz-tooltip [nzTooltipTitle]="hata()">⚠️</span> }
      </div>

      <!-- İçerik — ISablon varsa SablonRenderer -->
      @if (widget().sablon) {
        <sablon-renderer [sablon]="widget().sablon"
                         [parametreDegerleri]="veriParametreleri()"
                         [duzenlenebilir]="false" />
      } @else if (widget().stil) {
        <!-- Fallback: basit stil kartı -->
        <div class="widget-basit" [style.background]="widget().stil.gradyan || widget().stil.arkaPlan">
          <!-- Default render -->
        </div>
      }

      <!-- Item listesi -->
      @if (widget().itemler?.length) {
        <div class="widget-itemler">
          @for (item of widget().itemler; track item.id) {
            @if (item.sablon) {
              <sablon-renderer [sablon]="item.sablon"
                               [parametreDegerleri]="itemParametreleri(item)" />
            } @else {
              <div class="widget-item-basit" (click)="itemTiklandi(item)">
                @if (item.simge) { <span>{{ item.simge }}</span> }
                @if (item.baslik) { <span>{{ item.baslik }}</span> }
              </div>
            }
          }
        </div>
      }
    </div>
  `
})
```

---

## 11. Widget Editörü

NzDrawer ile açılır (sağdan):
- **Genel:** Başlık, simge, açıklama
- **Veri Kaynağı:** mod seçimi, VT, pipeline (JSON textarea), yenileme aralığı, soket event, sabit veri
- **Tasarım:** Sablon editörü (mevcut SablonEditoruComponent reuse) veya basit stil form
- **Olaylar:** Olay adı + JS kod editörü (textarea, syntax highlighting opsiyonel)
- **Pozisyon:** Grid genişlik/yükseklik (number input)
- **İtemler:** Item listesi, her item için JSON editörü + alt sablon editörü
- **Önizleme:** Canlı veri ile önizleme (widget-renderer embed)
- **Drag & Drop:** CDK DragDrop (mevcut SablonEditoruComponent'te de kullanılıyor)
  - Grid modu: CDK DragDrop `sira` bazlı reorder (1D liste — `cdkDropList` + `cdkDrag`, drop sonrası `sira` güncelle)
  - Serbest modu: CdkDrag free position (sablon editörüyle aynı — `cdkDragFreeDragPosition`, drop sonrası x/y güncelle)

---

## 12. Mevcut Ana Sayfa Dönüşümü

Ana sayfa route `''` replace edilir — `dashboard.component` yüklenir:
- Statik `kartlar` dizisi → `VARSAYILAN_WIDGETLER`'deki `sys-uygulamalar` widget'ına taşınır
- `uygulama-grid` → `dashboard.component`'in CSS Grid'i
- Mevcut uygulama kartları bir widget olarak render edilir
- **Fallback:** Mevcut `kartlar` dizisi korunur — `tumWidgetler()` boşsa veya hata olursa statik kartlar gösterilir
- `dashboard.component.ngOnInit` → `localStorage.getItem('fikoyt_cihaz_kimlik')` → `dashboardService.yukle(cihazId)`

---

## 13. Güvenlik & Hata Yönetimi

- Widget olayları `new Function('veri','widget','router','bildirim','guncelle', kod)` ile oluşturulur
- Her olay çağrısı try-catch ile sarmalanır, hata console.warn ile loglanır
- `piFunc` ve `donusumFunc` ayrı ayrı try-catch ile sarmalanır, hata durumunda widget'ın `hata()` signal'ı set edilir
- Pipeline'lar sunucu tarafında aggregate olarak çalışır
- Yetki: `widget.yetki` dizisi ile kullanıcı yetkilerine göre filtreleme
- DB kayıtları `sahip` alanı ile cihaz bazlı izolasyon
- DB sorgularında `silindi: { $ne: true }` filtresi uygulanır
- Widget veri çekme hatası → `hata()` signal'ı set edilir, UI'da ⚠️ gösterilir
- Soket kopması → fallback polling devreye girer
- Boş veri → "Veri yok" placeholder gösterilir

---

## 14. Dosya Listesi

| Dosya | Amaç |
|-------|------|
| `dashboard/interfaces/widget.interface.ts` | IWidget, IWidgetItem, IWidgetVeriKaynagi |
| `dashboard/hazir-widgetler.ts` | TS varsayılan widget tanımları + ISablon'lar |
| `dashboard/dashboard.service.ts` | CRUD + merge + veri çekme + soket + cache |
| `dashboard/dashboard.component.{ts,html,less}` | CSS Grid + CDK DragDrop + düzenleme modu |
| `dashboard/widget-renderer/widget-renderer.component.{ts,html,less}` | Widget render (OnPush) |
| `dashboard/widget-editor/widget-editor.component.{ts,html,less}` | Widget düzenleme (NzDrawer) |
| `hmi-pano.routes.ts` | Route güncelleme (ana-sayfa → dashboard) |
| `ana-sayfa.component.ts` | Dashboard entegrasyonu |
