diff --git a/.gitignore b/.gitignore index 8e0c0150..62775ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ configs/**/data/** *.bak **/node_modules **/.DS_Store -logs/ \ No newline at end of file +logs/ + +# Build sırasında git tag'lerinden üretilir (api/scripts/generate-release-notes.sh). +# Takip edilirse deploy'daki `git pull` yerel değişiklikle çakışır ve dosya eskir. +api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json \ No newline at end of file diff --git a/api/scripts/generate-release-notes.sh b/api/scripts/generate-release-notes.sh index 337c5a2a..3a1e30c0 100644 --- a/api/scripts/generate-release-notes.sh +++ b/api/scripts/generate-release-notes.sh @@ -2,26 +2,29 @@ set -e # Sürüm notlarını git tag'lerinden üretir: -# api/release-notes/release-notes.json +# api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json # -# Bu dosya API konteynerine /etc/api/release-notes altına MOUNT edilir (imaja -# GÖMÜLMEZ). Uygulama açılışında ReleaseNoteStartupService onu `Sas_H_ReleaseNote` -# tablosuyla eşitler. Yani yeni bir sürüm tag'i için ne DbMigrator ne de imaj -# derlemesi gerekir; deploy sırasında bu script koşar ve konteyner yeniden -# başlar. İstemci sürüm numarasını ve changelog'u application-configuration -# yanıtındaki `extraProperties.changeLogs` alanından okur. +# Dosya csproj'daki `Content Update="release-notes.json"` kuralıyla publish +# çıktısına kopyalanır, yani imaja gömülür ve konteynerde ContentRootPath +# altında (/srv/app) yer alır. ReleaseNoteStartupService açılışta tam bu yolu +# okuyup `Sas_H_ReleaseNote` tablosuyla eşitler; DbMigrator gerekmez. # -# Kaynak ağacının DIŞINDA bir klasöre yazılmasının sebebi Docker katman cache'i: -# dosya `src/` altında olsaydı Dockerfile'daki `COPY . .` her tag'de geçersizleşip -# tüm `dotnet publish` adımını yeniden çalıştırırdı. +# ÖNEMLİ: Çıktı yolu ReleaseNoteStartupService'in okuduğu yolla aynı olmak +# ZORUNDA. Daha önce script `api/release-notes/` altına yazıyordu; orası ne +# publish çıktısına kopyalanıyor ne de konteynere mount ediliyordu, dolayısıyla +# imajdaki eski (commit'lenmiş) dosya kullanılıyor ve yeni tag'ler hiç +# görünmüyordu. # -# Deploy akışında deploy/app.sh tarafından otomatik çağrılır; elle de -# çalıştırılabilir. Çıktı git'e commit EDİLMEZ (.gitignore). +# Çıktı .gitignore'dadır: takip edilseydi deploy'daki `git pull` her seferinde +# yerel değişiklikle çakışırdı. +# +# Deploy akışında configs/deployment/scripts/build/api.sh tarafından otomatik +# çağrılır; elle de çalıştırılabilir. # # Dış bağımlılığı yoktur (jq/node gerekmez): JSON kaçışları sed ile yapılır. cd "$(dirname "$0")/.." -OUTPUT="release-notes/release-notes.json" +OUTPUT="src/Sozsoft.Platform.HttpApi.Host/release-notes.json" mkdir -p "$(dirname "$OUTPUT")" if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Extensions/ReleaseNoteStartupService.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Extensions/ReleaseNoteStartupService.cs index 42b63910..b246872b 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Extensions/ReleaseNoteStartupService.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Extensions/ReleaseNoteStartupService.cs @@ -108,8 +108,12 @@ public class ReleaseNoteStartupService : IHostedService using var scope = _serviceProvider.CreateScope(); var (inserted, updated, deleted) = await SyncItemsAsync(scope.ServiceProvider, items); + // En son sürüm de loglanır: dosya eskiyse (üretim adımı atlanmış + // veya yanlış yola yazılmışsa) "0 eklendi" tek başına yanıltıcı olur. _logger.LogInformation( - "Sürüm notları senkronize edildi: {Total} tag, {Inserted} eklendi, {Updated} güncellendi, {Deleted} silindi.", + "Sürüm notları senkronize edildi ({Path}, en son sürüm {Latest}): {Total} tag, {Inserted} eklendi, {Updated} güncellendi, {Deleted} silindi.", + path, + items[0].Version, items.Count, inserted, updated, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json b/api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json index 5ff35f57..413de7f9 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json +++ b/api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json @@ -1,15 +1,25 @@ { "ReleaseNotes": [ { - "version": "1.2.0", + "version": "1.2.1", "releaseDate": "2026-08-13", - "commit": "798acbef9e6a26e29e58f10b13cb8e63ac5fa6b4", + "commit": "ee121db5211ffbd28496b87ce9e4776ff96f7640", "notes": [ - "Claude.ai optimizasyon güncellemesi", + "Zaafiyet testleri ve güncellemeleri", + "React 19 upgrade", + "Devexpress 26.1.4", "Migrations ve Seeds klasör düzenlemesi", "New Version Check komponent düzenlemesi" ] }, + { + "version": "1.1.11", + "releaseDate": "2026-08-11", + "commit": "783c1fe1d35f7cb8f23fb63fad26123cbe54aafc", + "notes": [ + "Claude.ai optimizasyon güncellemesi" + ] + }, { "version": "1.1.10", "releaseDate": "2026-07-31", diff --git a/configs/deployment/scripts/build/api.sh b/configs/deployment/scripts/build/api.sh index 4e071e07..1c843722 100644 --- a/configs/deployment/scripts/build/api.sh +++ b/configs/deployment/scripts/build/api.sh @@ -1,3 +1,9 @@ +#!/bin/bash +# set -e: sürüm notu üretimi sessizce başarısız olursa imaj eski/eksik notlarla +# derlenmesin. Daha önce böyle olmuştu: dosya git'te takip edildiği için hata +# fark edilmiyordu. +set -e + cd ~/sozsoft-platform git checkout main # --tags --force: sürüm notları git tag'lerinden üretiliyor; yerel tag origin'den @@ -8,7 +14,19 @@ cd ~/sozsoft-platform/api # Sürüm notları git tag'lerinden üretilir ve API imajına gömülür; uygulama # açılışta bunları veritabanına senkronize eder (DbMigrator gerekmez). -./scripts/generate-release-notes.sh +# `bash ...` ile çağrılır: depo Windows'tan yönetildiği için çalıştırma bitinin +# kaybolması "Permission denied" verip sürüm notlarını sessizce düşürüyordu. +RELEASE_NOTES=src/Sozsoft.Platform.HttpApi.Host/release-notes.json +bash scripts/generate-release-notes.sh + +# Dosya publish çıktısına csproj üzerinden girer; yoksa imaj sürüm notsuz kalır +# ve API açılışta "release-notes.json bulunamadı" uyarısı verir. Sessizce devam +# etmek yerine burada dur. +if [ ! -s "$RELEASE_NOTES" ]; then + echo "HATA: ${RELEASE_NOTES} üretilemedi, imaj derlenmiyor." >&2 + exit 1 +fi + VERSION=$(cat ~/sozsoft-platform/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json \ | grep -o '"[^"]*"\s*:\s*"[^"]*"' \ | grep -E '^"(Version)"' \ diff --git a/ui/src/components/template/UserDropdown.tsx b/ui/src/components/template/UserDropdown.tsx index 6fb4a21f..c16fc19d 100644 --- a/ui/src/components/template/UserDropdown.tsx +++ b/ui/src/components/template/UserDropdown.tsx @@ -11,6 +11,7 @@ import { Link } from 'react-router-dom' import { Avatar } from '../ui' import { FcEngineering, + FcNews, FcNext, FcNfcSign, FcSynchronize, @@ -142,6 +143,18 @@ const UserDropdownBase = ({ className }: CommonProps) => { + + + + + + {translate('::App.ChangeLog')} + + + {/* */} diff --git a/ui/src/store/locale.model.ts b/ui/src/store/locale.model.ts index 26cacd31..4a4b443b 100644 --- a/ui/src/store/locale.model.ts +++ b/ui/src/store/locale.model.ts @@ -7,6 +7,8 @@ export interface LocaleStoreModel { currentLang: string currentUiVersion: string | undefined currentTenantName?: string + /** Dolu ise kullanıcı bir sonraki girişinde ChangeLog ekranına yönlendirilir. */ + pendingChangelogVersion?: string } export interface LocaleStoreActions { @@ -14,6 +16,7 @@ export interface LocaleStoreActions { onSetLang: ThunkOn setUiVersion: Action setTenantName: Action + clearPendingChangelog: Action } export type LocaleModel = LocaleStoreModel & LocaleStoreActions @@ -22,6 +25,7 @@ const initialState: LocaleStoreModel = { currentLang: appConfig.locale, currentUiVersion: appConfig.uiVersion, currentTenantName: undefined, + pendingChangelogVersion: undefined, } export const localeModel: LocaleModel = { @@ -36,10 +40,41 @@ export const localeModel: LocaleModel = { helpers.getStoreActions().abpConfig.getConfig(false) }, ), + /** + * Backend'den gelen çalışan sürümü işler (application-configuration → + * `extraProperties.changeLogs.uiVersion`). + * + * Sürüm son görülenden farklıysa changelog bayrağı yazılır; kullanıcı bir + * sonraki girişinde ChangeLog ekranına yönlendirilir. Tetikleyici bilerek + * bundle değil sürümdür: yalnızca yeni bir git tag'i eklendiğinde de kullanıcı + * değişiklikleri görür. + * + * Referans ayrı bir alan değil `currentUiVersion`'ın kendisidir; zaten persist + * edildiği için "son görülen sürüm" bilgisini o taşır. Ayrı bir alan eklemek + * mevcut kullanıcılarda bir sürümlük boşluk yaratırdı (yeni alan boş olduğu + * için ilk deploy sessizce atlanır). + * + * Yeni tarayıcıda `currentUiVersion` boştur (`appConfig.uiVersion = undefined`); + * kullanıcı ilk girişinde changelog'a düşmesin diye bayrak yazılmaz. + * + * Durum bu model üzerinden persist edilir (`[EasyPeasyStore][0].locale`); + * ayrı `localStorage` anahtarı KULLANILMAZ. + */ setUiVersion: action((state, payload) => { + // Config henüz yüklenmediğinde çağrı boş gelir; persist edilmiş "son görülen + // sürüm" bu yüzden ezilmemeli, yoksa her açılışta referans sıfırlanır ve + // bayrak bir daha hiç yazılmaz. + if (!payload) return + + const lastSeen = state.currentUiVersion state.currentUiVersion = payload + + if (lastSeen && lastSeen !== payload) state.pendingChangelogVersion = payload }), setTenantName: action((state, payload) => { state.currentTenantName = payload }), + clearPendingChangelog: action((state) => { + state.pendingChangelogVersion = undefined + }), } diff --git a/ui/src/utils/hooks/useAuth.ts b/ui/src/utils/hooks/useAuth.ts index 0cf960b7..f10a44d9 100644 --- a/ui/src/utils/hooks/useAuth.ts +++ b/ui/src/utils/hooks/useAuth.ts @@ -1,7 +1,7 @@ import { jwtDecode } from 'jwt-decode' import { useNavigate } from 'react-router-dom' import useQuery from './useQuery' -import { useStoreActions, useStoreState } from '../../store/store' +import { store, useStoreActions, useStoreState } from '../../store/store' import { FailedSignInResponse, SignInCredential, @@ -13,7 +13,6 @@ import { isLoginSuccess } from '../../proxy/account/models' import { REDIRECT_URL_KEY } from '../../constants/app.constant' import appConfig from '../../proxy/configs/app.config' import { register } from '../../services/account.service' -import { hasPendingChangelog } from '@/views/version/swRegistration' import { ROUTES_ENUM } from '@/routes/route.constant' type Status = 'success' | 'failed' | 'error' @@ -72,7 +71,11 @@ function useAuth() { const redirectUrl = query.get(REDIRECT_URL_KEY) // Uygulama yeni bir sürümle/deploy ile açıldıysa kullanıcı önce // değişiklikleri görsün. Bayrak ChangeLog ekranında tüketilir. - const target = hasPendingChangelog() + // + // Değer hook state'inden değil doğrudan store'dan okunur: bayrak giriş + // sayfasındaki config yüklemesiyle yazılıyor, render kapanışı bayat + // kalabilir. + const target = store.getState().locale.pendingChangelogVersion ? ROUTES_ENUM.protected.admin.changeLog : (redirectUrl ?? appConfig.authenticatedEntryPath) navigate(target) diff --git a/ui/src/views/version/ChangeLog.tsx b/ui/src/views/version/ChangeLog.tsx index 9b10adfe..c54200b4 100644 --- a/ui/src/views/version/ChangeLog.tsx +++ b/ui/src/views/version/ChangeLog.tsx @@ -6,7 +6,8 @@ import React, { useEffect, useState } from 'react' import PageTitle from '@/components/shared/PageTitle' import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa' import { releaseNoteService, type ReleaseNoteDto } from '@/services/releaseNote.service' -import { checkForAppUpdate, clearPendingChangelog } from './swRegistration' +import { checkForAppUpdate } from './swRegistration' +import { useStoreActions } from '@/store' // User dropdown reuses the exact update-check flow owned by this component. // eslint-disable-next-line react-refresh/only-export-components @@ -106,6 +107,7 @@ const Log = ({ const Changelog = () => { const { translate } = useLocalization() const { isCheckingUpdate, checkForUpdate } = useManualUpdateCheck() + const clearPendingChangelog = useStoreActions((actions) => actions.locale.clearPendingChangelog) // Kaynak `ReleaseNote` entity'sidir; liste yalnızca bu ekran açıldığında // istenir (application-configuration sadece çalışan sürümü taşır). @@ -120,7 +122,7 @@ const Changelog = () => { .getList() .then(setReleases) .catch((error: unknown) => console.warn('Sürüm notları alınamadı.', error)) - }, []) + }, [clearPendingChangelog]) return ( diff --git a/ui/src/views/version/swRegistration.ts b/ui/src/views/version/swRegistration.ts index 7bf272f0..998e7e99 100644 --- a/ui/src/views/version/swRegistration.ts +++ b/ui/src/views/version/swRegistration.ts @@ -19,10 +19,11 @@ import './installPrompt' * 2. Yeni worker bulunursa kurulum başlar; `src/sw.ts` indirdiği her dosyayı * isim/boyut/yüzde olarak yayınlar, bu modül state'e çevirir. * 3. Kurulum bitince SKIP_WAITING → `controllerchange` → reload. - * 4. Backend'den gelen sürüm son görülenden farklıysa changelog bayrağı - * yazılır; login sonrası kullanıcı ChangeLog ekranına yönlendirilir. - * Tetikleyici sürümdür, bundle değil: yalnızca yeni bir sürüm notu (git - * tag) eklendiğinde de kullanıcı değişiklikleri görür. + * 4. Changelog yönlendirmesi bu modülün işi DEĞİLDİR: son görülen sürüm ve + * bekleyen changelog bayrağı store'un `locale` diliminde tutulur + * (`setUiVersion` / `pendingChangelogVersion`), böylece durum tek yerde ve + * `[EasyPeasyStore][0]` içinde persist edilir. Bu modül yalnızca güncelleme + * penceresinin gösterdiği sürüm/not bilgisini tutar. * * Bu dosya entry chunk'ındadır: React/UI/store bağımlılığı yoktur, pencere * yalnızca gerçekten indirme varken lazy yüklenir. @@ -111,14 +112,22 @@ const VERSION_POLL_INTERVAL = 30 * 60 * 1000 const MIN_CHECK_GAP = 30 * 1000 const ACTIVATION_RETRY_DELAY = 8_000 const ACTIVATION_TIMEOUT = 45_000 +/** İndirme bittikten sonra geçişin kendiliğinden başlaması için tanınan süre. */ +const ACTIVATION_HANDOFF_TIMEOUT = 15_000 const REGISTRATION_TIMEOUT = 10_000 const UPDATE_CALL_TIMEOUT = 20_000 /** Kurulum/aktivasyon başarısız olduğunda otomatik yeniden deneme süresi. */ const RETRY_DELAY_SECONDS = 15 -const LAST_VERSION_KEY = 'sozsoft:last-version' -const PENDING_CHANGELOG_KEY = 'sozsoft:pending-changelog' -const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload' +/** + * Chunk yeniden yükleme koruması: sonsuz reload döngüsünü keser. + * + * Store'da tutulamaz — reload'ın hemen öncesinde, React/store mount edilmeden + * yazılması gerekiyor. Bu yüzden modüldeki tek doğrudan depo anahtarı budur; + * sürüm/changelog durumu store'un `locale` diliminde durur. Bayrak 10 saniye + * sonra silindiği için kalıcı bir kayıt bırakmaz. + */ +const CHUNK_RELOAD_GUARD_KEY = 'chunkReloadGuard' /** Service worker yalnızca prod'da, dev'de ise VITE_PWA_DEV=true ile aktiftir. */ const isServiceWorkerEnabled = import.meta.env.PROD || import.meta.env.VITE_PWA_DEV === 'true' @@ -129,19 +138,18 @@ const withTimeout = (promise: Promise, ms: number) => new Promise((resolve) => window.setTimeout(() => resolve(undefined), ms)), ]) -const readStorage = (key: string, session = false) => { +const readStorage = (key: string) => { try { - return (session ? sessionStorage : localStorage).getItem(key) ?? undefined + return localStorage.getItem(key) ?? undefined } catch { return undefined } } -const writeStorage = (key: string, value: string | undefined, session = false) => { +const writeStorage = (key: string, value: string | undefined) => { try { - const store = session ? sessionStorage : localStorage - if (value === undefined) store.removeItem(key) - else store.setItem(key, value) + if (value === undefined) localStorage.removeItem(key) + else localStorage.setItem(key, value) } catch { /* storage kapalıysa yoksay */ } @@ -240,9 +248,13 @@ function recalculate( /* ------------------------------------------------------------------ */ /** - * Sürüm bilgisini application-configuration yanıtından alır. + * Sürüm bilgisini application-configuration yanıtından alır ve yalnızca bu + * modülün GÖRÜNTÜLEME durumunu (güncelleme penceresi) besler. + * + * Kalıcı sürüm takibi — son görülen sürüm ve changelog bayrağı — burada değil, + * store'un `locale` diliminde tutulur (`setUiVersion`). Bu modül entry chunk'ta + * olduğu için store'a bağımlı olmamalı; bayrağı yazan taraf store'dur. * - * Store, config'i her yüklediğinde bunu çağırır; ayrı bir sürüm isteği yoktur. * Yük şekli: `extraProperties.changeLogs = { uiVersion, tags[] }`. */ export const setAppVersionInfo = (raw: unknown) => { @@ -253,29 +265,6 @@ export const setAppVersionInfo = (raw: unknown) => { currentVersion: data.uiVersion, changeLog: Array.isArray(data.tags) ? data.tags.filter(Boolean) : [], }) - - markVersionSeen(data.uiVersion) -} - -/** Login sonrası changelog yönlendirmesi için bayrak. */ -export const hasPendingChangelog = () => !!readStorage(PENDING_CHANGELOG_KEY) - -export const clearPendingChangelog = () => writeStorage(PENDING_CHANGELOG_KEY, undefined) - -/** - * Sürüm numarası son görülenden farklıysa changelog bayrağını yazar; kullanıcı - * bir sonraki girişinde ChangeLog ekranına yönlendirilir. - * - * Tetikleyici bilerek BUNDLE DEĞİL, backend'den gelen sürümdür: yalnızca yeni - * bir sürüm notu (git tag) eklendiğinde de — hiçbir bileşen değişmemiş olsa - * bile — kullanıcı değişiklikleri görür. İlk açılışta (kayıt yokken) bayrak - * yazılmaz, yalnızca mevcut sürüm not edilir. - */ -function markVersionSeen(version: string) { - const lastSeen = readStorage(LAST_VERSION_KEY) - if (lastSeen === version) return - if (lastSeen) writeStorage(PENDING_CHANGELOG_KEY, version) - writeStorage(LAST_VERSION_KEY, version) } /* ------------------------------------------------------------------ */ @@ -291,6 +280,7 @@ let lastCheckAt = 0 let pollTimer: number | undefined let activationRetryTimer: number | undefined let activationTimeoutTimer: number | undefined +let activationHandoffTimer: number | undefined const watchedRegistrations = new WeakSet() const watchedWorkers = new WeakSet() @@ -298,8 +288,39 @@ const watchedWorkers = new WeakSet() function clearActivationTimers() { window.clearTimeout(activationRetryTimer) window.clearTimeout(activationTimeoutTimer) + window.clearTimeout(activationHandoffTimer) activationRetryTimer = undefined activationTimeoutTimer = undefined + activationHandoffTimer = undefined +} + +/** + * İndirme bitti ('ready') ama geçişi kimse başlatmadıysa devreye girer. + * + * Normal akışta geçişi worker'ın `installed` olayı tetikler. O olay kaçabiliyor: + * dinleyici yalnızca `reg.installing` görüldüğünde kuruluyor ve sayfa hard reload + * ile açıldığında kayıt zaten `waiting` durumunda olabiliyor. Watchdog olmadan + * durum sonsuza kadar 'ready'de kalıyordu — ekran "yeni sürüme geçiliyor"da + * donuyor, zaman aşımı da yoktu çünkü sayaçlar yalnızca aktivasyon başladıktan + * sonra kuruluyor. + */ +function scheduleActivationHandoff() { + if (activationHandoffTimer || activating || reloading) return + + activationHandoffTimer = window.setTimeout(() => { + activationHandoffTimer = undefined + if (activating || reloading) return + + if (registration?.waiting) { + activateWaitingWorker() + return + } + + // Bekleyen worker yok: geçiş sessizce kaybolmuş. Kabuk cache'i tazelenerek + // yenilenir, aksi halde aynı eski index.html geri gelir. + console.warn('Yeni sürüme geçiş tetiklenmedi, sayfa yenileniyor.') + void reloadWithFreshShell() + }, ACTIVATION_HANDOFF_TIMEOUT) } function reloadForUpdate() { @@ -539,6 +560,12 @@ function handleWorkerMessage(event: MessageEvent) { */ const transitions = controlled && data.phase !== 'warmup' + // Isıtma kurulumla aynı anda koşabiliyor (eski aktif worker ısıtırken yenisi + // kuruluyor). İki oturumun dosya listeleri ayrı index uzayında olduğu için + // birleşince satırlar tekrarlanıyor ve toplam boyut aşıyordu; geçiş sürecinde + // ısıtma mesajları yok sayılır. + if (data.phase === 'warmup' && (activating || state.status === 'ready')) return + const totals = { loadedBytes: data.loadedBytes, totalBytes: data.totalBytes } switch (data.type) { @@ -575,7 +602,8 @@ function handleWorkerMessage(event: MessageEvent) { visible: true, percent: 100, }) - if (!transitions) hideDialogLater() + if (transitions) scheduleActivationHandoff() + else hideDialogLater() break case 'SW_PRECACHE_ERROR': if (!transitions) { @@ -714,8 +742,14 @@ function scheduleWarmup(reg: ServiceWorkerRegistration) { function trackInstallingWorker(reg: ServiceWorkerRegistration) { const worker = reg.installing - // Controller yoksa bu ilk kurulumdur; kullanıcıya güncelleme demek yanlış olur. - if (!worker || !navigator.serviceWorker.controller || watchedWorkers.has(worker)) return + if (!worker || watchedWorkers.has(worker)) return + + // İlk kurulumda kullanıcıya "güncelleme" demek yanlış olur. Ölçüt yalnızca + // `controller` olamaz: hard reload'da sayfa kontrolsüz açılabiliyor, ama ortada + // güncellenen bir kayıt (aktif worker) var. Bu ayrım yapılmazsa dinleyici hiç + // kurulmuyor ve geçişi tetikleyecek `installed` olayı kaçıyordu. + const isUpdate = !!navigator.serviceWorker.controller || !!reg.active + if (!isUpdate) return watchedWorkers.add(worker) void showUpdateDialog('downloading') @@ -776,8 +810,8 @@ function startWatchers() { function handleChunkLoadFailures() { window.addEventListener('vite:preloadError', (event) => { if (reloading) return - if (readStorage(CHUNK_RELOAD_GUARD_KEY, true)) return - writeStorage(CHUNK_RELOAD_GUARD_KEY, '1', true) + if (readStorage(CHUNK_RELOAD_GUARD_KEY)) return + writeStorage(CHUNK_RELOAD_GUARD_KEY, '1') event.preventDefault() console.warn('Eksik chunk tespit edildi, sayfa yenileniyor.') reloading = true @@ -785,7 +819,7 @@ function handleChunkLoadFailures() { }) // Sayfa sorunsuz açıldıysa koruma bayrağını temizle. - window.setTimeout(() => writeStorage(CHUNK_RELOAD_GUARD_KEY, undefined, true), 10_000) + window.setTimeout(() => writeStorage(CHUNK_RELOAD_GUARD_KEY, undefined), 10_000) } /** Kapalıyken önceki oturumlardan kalan kayıtları temizler. */