# Sözsoft Platform UI React 19 + Vite 8 (rolldown) + DevExtreme + easy-peasy + Tailwind 4. Ekranların tamamı veritabanından gelen ListForm/menü/route tanımlarıyla çizilir; bu klasör yalnızca çekirdek motoru taşır. Platformun geneli için [README.md](../README.md), çalışma kuralları için [CLAUDE.md](../CLAUDE.md). ## Komutlar | Komut | Ne yapar | | --- | --- | | `npm start` | Dev sunucusu (3000). `prestart` DevExtreme lisans dosyasını üretir. | | `npm run build` | Üretim derlemesi → `dist/`. `--mode dev|production` ile `.env.{mode}` okunur. | | `npm run preview` | Yerel önizleme; üretim sunucusu **değildir** (bkz. Konteyner). | | `npm run typecheck` / `npm run lint` | `tsc --noEmit` / `oxlint`. Her değişiklikten sonra ikisi de 0 hata vermeli. | | `npm run format` | prettier + oxlint --fix. | Ortam değişkenleri `.env` (yerel), `.env.dev`, `.env.production`: `VITE_API_URL`, `VITE_CDN_URL`. Node/npm sürümü `package.json` → `volta`. ## Derleme çıktısı `vite.config.ts` üç şey üretir: - **Chunk'lar** `assets/js/entry-*.js` (giriş), `assets/js/vendor-react-*.js`, `vendor-router-*.js` (her açılışta yüklenen, sürümler arası değişmeyen çekirdek) ve rolldown'un otomatik böldüğü lazy chunk'lar; CSS `assets/css/`, görsel/font `assets/img|fonts/`. Adlar hash'lidir: aynı ad = aynı içerik. - **`sw.js`** — `src/sw.ts`'ten (`vite-plugin-pwa`, `injectManifest`). Precache manifest'i yalnızca kabuğu (`PRECACHE_GLOBS`) taşır. - **`warmup.json`** — `warmupManifest()` eklentisi: bütün JS/CSS chunk'larının adı/boyutu ve her `views/**` modülünün açılması için gereken dosya zinciri (`routes`). Service worker ısıtma ve eski sürüm temizliği için okur. `twSafelistGenerator.ts`: custom component kodu veritabanında yaşadığı için Tailwind'in göremediği sınıflar `safelist.txt`'ten derlemeye eklenir. ## Rota ve bileşen çözümü `src/routes/dynamicRouteLoader.ts` `import.meta.glob('../views/**/*.tsx')` ile bütün view modüllerini lazy olarak kaydeder; `App.Menus.Routes` kaydındaki `componentPath` bu haritadan çözülür (`hasPhysicalComponent`), karşılığı olmayan rota router'a alınmaz. Runtime bileşenler (`Custom Components`) `ComponentContext` üzerinden çizilir ve aynı yolu paylaşan fiziksel rotayı gölgeler. Ayrıntı: [README.md §18.1](../README.md). ## PWA ve sürüm geçişi Kod: `src/sw.ts` (service worker), `src/views/version/swRegistration.ts` (kayıt/güncelleme motoru, entry chunk'ta, React'a bağımlı değil), `AppUpdateDialog.tsx` (lazy, kendi React kökünde mount edilir), `installPrompt.ts` (PWA kurulum teklifi). 1. **Kurulum = kabuk.** `install` olayı yalnızca precache manifest'indeki dosyaları indirir (index.html, entry JS/CSS, vendor chunk'ları). Biri inmezse kurulum düşer; yarım sürümle aktive olunmaz. İndirme ilerlemesi dosya dosya istemciye yayınlanır. 2. **Isıtma = bileşenler.** Oturum açılınca store, yetkili `protected` rotaların view modüllerini `setWarmupRoutes` ile bildirir; worker `ENSURE_WARMUP` ile `warmup.json`'daki zinciri arka planda indirir. Ziyaretçi ve giriş ekranı hiçbir bileşen indirmez. Best-effort: inemeyen dosya atlanır, süre bütçesi dolunca kalanlar runtime cache'e kalır. 3. **Güncelleme.** Deploy'da herhangi bir chunk değişince entry hash'i, index.html ve `sw.js` manifest'i değişir; `registration.update()` (kayıt, sekmeye dönüş, bağlantı, 30 dk yoklama) yeni worker'ı görür → kurulum → `SKIP_WAITING` → `controllerchange` → yenileme. Pencere geçiş bitene kadar kapanmaz; ilk kurulumda (controller yok) kapatılabilir. 4. **Sürüm numarası ve notlar backend'dedir** (`extraProperties.changeLogs`, `/api/app/change-log`); UI'da sürüm dosyası yoktur. Son görülen sürüm store'un `locale` diliminde tutulur, giriş sonrası changelog yönlendirmesi oradan okunur. 5. **Runtime cache.** Hash'li dosyalar cache-first (`sozsoft-assets-v1`, aktivasyonda `warmup.json`'da olmayan eski chunk'lar silinir), DevExtreme temaları stale-while-revalidate, font/görsel cache-first. `/api`, `/connect`, `/swagger`, `/signalr`, `/hangfire` worker'a girmez. Gezinmeler kabuk cache'indeki index.html ile anında açılır, kopya arka planda tazelenir. ### Deploy sonrası eksik / değişmiş chunk İki katman birlikte çalışır: - **Sunucu tarafı (asıl çözüm):** konteyner nginx'i hash'li `assets/` dosyalarını `/srv/assets` volume'undan servis eder; `nginx/40-archive-assets.sh` her açılışta yeni sürümün dosyalarını arşive ekler (var olanı ezmez), imajda artık olmayan ve 30 günden eski dosyaları siler. Açık sekmeler deploy'dan sonra da kendi sürümlerinin chunk'larını bulur. - **İstemci tarafı (yedek):** bir chunk yine de bulunamazsa Vite `vite:preloadError` fırlatır; `handleChunkLoadFailures` kabuk cache'ini silip sayfayı **bir kez** yeniler (`reloadWithFreshShell`; düz `reload` eski index.html'i geri getirirdi). Koruma bayrağı (`localStorage.chunkReloadGuard`, 10 s) döngüyü keser; ikinci başarısızlık hata sınırına düşer, `Views.tsx` `isChunkLoadError` ile "uygulama güncellendi, yenileyin" mesajı ve `reloadApp` butonu gösterir. Hata sınırı rota değişince kendini sıfırlar (`resetKeys`). Dev'de service worker kapalıdır (`VITE_PWA_DEV=true` ile açılır); kapalıyken önceki kayıtlar temizlenir, yoksa `sw.js` yerine index.html döner ve "unsupported MIME type" hatası üretirdi. ## Konteyner `Sozsoft.Platform.Ui.Dockerfile` iki aşamalıdır: 1. `node:24.22-alpine` — `npm run build:production -- --mode $ENV`; DevExtreme lisansı yalnızca bu aşamada, tek `RUN` içinde yazılıp silinir. `.env.$ENV`'deki `VITE_API_URL` `/tmp/api-url` olarak ikinci aşamaya geçer. 2. `nginx:1.27-alpine` — `dist/` + `nginx/default.conf` (`${API_URL}` envsubst ile build'de doldurulur, `nginx -t` ile doğrulanır) + `/docker-entrypoint.d/40-archive-assets.sh`. `nginx/default.conf` sözleşmesi: | Yol | Cache-Control | Not | | --- | --- | --- | | `/assets/**` | `public, max-age=31536000, immutable` | `/srv/assets` (imaj + arşiv) | | `/index.html`, `/sw.js`, `/warmup.json`, `/manifest.webmanifest` | `no-store, no-cache, must-revalidate` | sürüm kapısı | | `/css/**` (DevExtreme temaları) | `public, max-age=86400` | hash'siz, SW tazeler | | `/img/**` | `public, max-age=604800` | | | `/sitemap.xml`, `/robots.txt` | — | API'ye proxy (`SeoController`) | | diğer | — | SPA fallback → `index.html` | Compose (`configs/deployment/docker-compose-app.yml`): `ui` servisi `3002:80`, volume `ui-assets:/srv/assets`. Deploy (`scripts/deploy/app.sh`) konteynerleri önceden durdurmaz: `pull` → `up -d`; kesinti yalnızca konteyner değişimi kadardır. Ters vekil `nginx.conf` Cache-Control başlıklarını ezmez; başlıklar konteynerden gelir, tenant alt alan adları da aynı başlıkları alır. ## Kod düzeni ``` src/ components/ paylaşılan bileşenler (ui/, shared/, template/, layouts/, list/form editörleri…) views/ rota bileşenleri (import.meta.glob ile lazy); views/shared/ hariç routes/ dinamik router ve route çözümü store/ easy-peasy modelleri (abpConfig, auth, locale, base…) services/ axios istemcileri (authApi, platformApi) ve SignalR servisleri proxy/ API DTO tipleri utils/ yardımcılar ve hook'lar sw.ts service worker ``` Grid/Tree popup'ı ile form sayfası editör yeteneklerini `views/list/shared/*` ve `form/editors/*` üzerinden paylaşır; `scriptRecipes.ts` C# `Domain.Shared/Editors/*` ile birebir aynı çıktıyı üretmek zorundadır (bkz. CLAUDE.md "Cross-cutting"). Grid ve Tree'nin ortak iç seçenekleri (düzenleme, araç çubuğu, süzgeçler, hücre editörü şablonları) tek yerde çizilir: `views/list/shared/listOptionChildren.tsx`. Ortak yardımcılar; aynı işin ikinci bir kopyası yazılmaz: | İş | Yardımcı | | --- | --- | | Kullanıcıya görünen metin | `useLocalization().translate('::Anahtar')`; React dışı kodda `utils/translate.ts` `translateStatic` | | Toast bildirimi | `utils/notify.tsx` `notifySuccess/Info/Warning/Danger(başlık?, { message })` | | API hatasının bildirimi | `services/apiError.ts` `pushApiError` | | Kimlik üretimi | `utils/id.ts` `createId` (UUID), `createShortId` | | Tarih/sayı/para/boyut | `utils/localeFormat.ts` (aşağıda) | ## Tarih ve sayı biçimi Kullanıcıya görünen her tarih ve sayı **aktif kültürle** biçimlenir; kültür `useLocale` ile store'dan ``'e yazılır ve tek giriş `src/utils/localeFormat.ts`'tir: | İhtiyaç | Çağrı | | --- | --- | | Tarih / saat / tarih+saat | `formatLocaleDate`, `formatLocaleTime`, `formatLocaleDateTime` (dayjs `L`, `LT`, `L LT`); uzun tarih `formatLocaleLongDate` (`LL`) | | Sayı | `formatLocaleNumber(v, { minimumFractionDigits, maximumFractionDigits, useGrouping })` — varsayılan binlik ayracı **yok** (yıl, kimlik, adet), tutar için `useGrouping: true` | | Sabit ondalık / dosya boyutu | `formatLocaleFixed(v, 2)`, `formatLocaleBytes(bytes)` | | Para | `formatLocaleCurrency(v, currencyCode)` — kod veriden gelir; verisi olmayan fiyat `DEFAULT_CURRENCY` (`constants/app.constant.ts`) | | Ham endpoint değeri | `formatLocaleValue(v)` (ISO tarih ve sayıyı çevirir, diğerini olduğu gibi bırakır) | Hepsi boş/geçersiz değer için `''` döner ve `Date | string | number | Dayjs` alır. Tarih seçiciler (`DatePicker`, `DateTimepicker`) `inputFormat` verilmeyince kültürün desenini kullanır; prop yalnızca makine biçimi gerektiğinde verilir. DevExtreme kolon/editör biçimleri **adlı** biçimdir: `shortDate`, `shortDateShortTime`, `shortTime`, `decimal`, `fixedPoint`, `currency` (`EditorOptions.Date/DateTime/Time` ↔ `presets.ts`). Kültür `DevExtremeBoundary` → `configureDevExtreme` ile verilir: `locale(cultureName)` tarih/sayıyı, `loadMessages` ile yüklenen `devextreme/localization/messages/{dil}.json` arayüz metinlerini (Evet/Hayır, "Veri yok", filtre satırı) çevirir; paket dışı dil İngilizce metinle kalır. Yazılmayanlar: `'DD.MM.YYYY'`, `'HH:mm'` gibi sabit dayjs desenleri, `toLocaleString()` / `toLocaleDateString('tr-TR')`, `Intl.*Format('tr-TR')`, görüntülenen ondalık için `toFixed`. Sunucu tarafı karşılığı `CultureFormat` + `CultureInfo.CurrentCulture`'dır. **Makine biçimi serbesttir** ve `localeFormat`'tan geçmez: `` değeri ve API yüküne giden ISO metin (`YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss`), gruplama/sorgu anahtarı, DevExtreme `dateSerializationFormat`, yakınlaştırma yüzdesi gibi sayısal hesap yuvarlaması (`Number(x.toFixed(2))`), metin karşılaştırması için `toLocaleLowerCase('tr-TR')`. `AppUpdateDialog` uygulama ağacının dışında, kültür bilinmeden açıldığı için kendi `formatBytes` kopyasını taşır.