2026-10-17 18:51:10 +00:00
# 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
2026-10-19 14:35:08 +00:00
modüllerini lazy olarak kaydeder; `Sas.Menus.Routes` kaydı ndaki `componentPath` bu haritadan
2026-10-17 18:51:10 +00:00
çö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`
2026-10-20 10:53:21 +00:00
diliminde tutulur; sürüm değişince yazı lan bayrağı `ProtectedRoute` okur ve oturum açı k
kullanı cı yı (login şartı olmadan) `/admin/changeLog` 'a yönlendirir, ekran bayrağı tüketir.
2026-10-17 18:51:10 +00:00
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
`<html lang>` '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: `<input type="date|datetime-local">` 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.