ReleaseNotes düzeltmesi

This commit is contained in:
Sedat ÖZTÜRK 2026-08-13 13:43:48 +03:00
parent ee121db521
commit cced417ede
10 changed files with 194 additions and 68 deletions

4
.gitignore vendored
View file

@ -3,3 +3,7 @@ configs/**/data/**
**/node_modules **/node_modules
**/.DS_Store **/.DS_Store
logs/ 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

View file

@ -2,26 +2,29 @@
set -e set -e
# Sürüm notlarını git tag'lerinden üretir: # 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 # Dosya csproj'daki `Content Update="release-notes.json"` kuralıyla publish
# GÖMÜLMEZ). Uygulama açılışında ReleaseNoteStartupService onu `Sas_H_ReleaseNote` # çıktısına kopyalanır, yani imaja gömülür ve konteynerde ContentRootPath
# tablosuyla eşitler. Yani yeni bir sürüm tag'i için ne DbMigrator ne de imaj # altında (/srv/app) yer alır. ReleaseNoteStartupService açılışta tam bu yolu
# derlemesi gerekir; deploy sırasında bu script koşar ve konteyner yeniden # okuyup `Sas_H_ReleaseNote` tablosuyla eşitler; DbMigrator gerekmez.
# başlar. İstemci sürüm numarasını ve changelog'u application-configuration
# yanıtındaki `extraProperties.changeLogs` alanından okur.
# #
# Kaynak ağacının DIŞINDA bir klasöre yazılmasının sebebi Docker katman cache'i: # ÖNEMLİ: Çıktı yolu ReleaseNoteStartupService'in okuduğu yolla aynı olmak
# dosya `src/` altında olsaydı Dockerfile'daki `COPY . .` her tag'de geçersizleşip # ZORUNDA. Daha önce script `api/release-notes/` altına yazıyordu; orası ne
# tüm `dotnet publish` adımını yeniden çalıştırırdı. # 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 # Çıktı .gitignore'dadır: takip edilseydi deploy'daki `git pull` her seferinde
# çalıştırılabilir. Çıktı git'e commit EDİLMEZ (.gitignore). # 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. # Dış bağımlılığı yoktur (jq/node gerekmez): JSON kaçışları sed ile yapılır.
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
OUTPUT="release-notes/release-notes.json" OUTPUT="src/Sozsoft.Platform.HttpApi.Host/release-notes.json"
mkdir -p "$(dirname "$OUTPUT")" mkdir -p "$(dirname "$OUTPUT")"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then

View file

@ -108,8 +108,12 @@ public class ReleaseNoteStartupService : IHostedService
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var (inserted, updated, deleted) = await SyncItemsAsync(scope.ServiceProvider, items); 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( _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, items.Count,
inserted, inserted,
updated, updated,

View file

@ -1,15 +1,25 @@
{ {
"ReleaseNotes": [ "ReleaseNotes": [
{ {
"version": "1.2.0", "version": "1.2.1",
"releaseDate": "2026-08-13", "releaseDate": "2026-08-13",
"commit": "798acbef9e6a26e29e58f10b13cb8e63ac5fa6b4", "commit": "ee121db5211ffbd28496b87ce9e4776ff96f7640",
"notes": [ "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", "Migrations ve Seeds klasör düzenlemesi",
"New Version Check komponent 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", "version": "1.1.10",
"releaseDate": "2026-07-31", "releaseDate": "2026-07-31",

View file

@ -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 cd ~/sozsoft-platform
git checkout main git checkout main
# --tags --force: sürüm notları git tag'lerinden üretiliyor; yerel tag origin'den # --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 # 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). # 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 \ VERSION=$(cat ~/sozsoft-platform/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json \
| grep -o '"[^"]*"\s*:\s*"[^"]*"' \ | grep -o '"[^"]*"\s*:\s*"[^"]*"' \
| grep -E '^"(Version)"' \ | grep -E '^"(Version)"' \

View file

@ -11,6 +11,7 @@ import { Link } from 'react-router-dom'
import { Avatar } from '../ui' import { Avatar } from '../ui'
import { import {
FcEngineering, FcEngineering,
FcNews,
FcNext, FcNext,
FcNfcSign, FcNfcSign,
FcSynchronize, FcSynchronize,
@ -142,6 +143,18 @@ const UserDropdownBase = ({ className }: CommonProps) => {
</span> </span>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item eventKey="ChangeLog" className="mb-1 px-0">
<Link
className="flex h-full w-full items-center gap-2 px-2"
to={ROUTES_ENUM.protected.admin.changeLog}
>
<span className="text-xl opacity-50">
<FcNews />
</span>
<span>{translate('::App.ChangeLog')}</span>
</Link>
</Dropdown.Item>
{/* <Dropdown.Item variant="divider" /> */} {/* <Dropdown.Item variant="divider" /> */}
<Dropdown.Item eventKey="Sign Out" className="mb-1 px-2" onClick={signOut}> <Dropdown.Item eventKey="Sign Out" className="mb-1 px-2" onClick={signOut}>
<span className="text-xl opacity-50"> <span className="text-xl opacity-50">

View file

@ -7,6 +7,8 @@ export interface LocaleStoreModel {
currentLang: string currentLang: string
currentUiVersion: string | undefined currentUiVersion: string | undefined
currentTenantName?: string currentTenantName?: string
/** Dolu ise kullanıcı bir sonraki girişinde ChangeLog ekranına yönlendirilir. */
pendingChangelogVersion?: string
} }
export interface LocaleStoreActions { export interface LocaleStoreActions {
@ -14,6 +16,7 @@ export interface LocaleStoreActions {
onSetLang: ThunkOn<LocaleModel, Injections, StoreModel> onSetLang: ThunkOn<LocaleModel, Injections, StoreModel>
setUiVersion: Action<LocaleStoreModel, string | undefined> setUiVersion: Action<LocaleStoreModel, string | undefined>
setTenantName: Action<LocaleStoreModel, string | undefined> setTenantName: Action<LocaleStoreModel, string | undefined>
clearPendingChangelog: Action<LocaleStoreModel, void>
} }
export type LocaleModel = LocaleStoreModel & LocaleStoreActions export type LocaleModel = LocaleStoreModel & LocaleStoreActions
@ -22,6 +25,7 @@ const initialState: LocaleStoreModel = {
currentLang: appConfig.locale, currentLang: appConfig.locale,
currentUiVersion: appConfig.uiVersion, currentUiVersion: appConfig.uiVersion,
currentTenantName: undefined, currentTenantName: undefined,
pendingChangelogVersion: undefined,
} }
export const localeModel: LocaleModel = { export const localeModel: LocaleModel = {
@ -36,10 +40,41 @@ export const localeModel: LocaleModel = {
helpers.getStoreActions().abpConfig.getConfig(false) 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) => { 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 state.currentUiVersion = payload
if (lastSeen && lastSeen !== payload) state.pendingChangelogVersion = payload
}), }),
setTenantName: action((state, payload) => { setTenantName: action((state, payload) => {
state.currentTenantName = payload state.currentTenantName = payload
}), }),
clearPendingChangelog: action((state) => {
state.pendingChangelogVersion = undefined
}),
} }

View file

@ -1,7 +1,7 @@
import { jwtDecode } from 'jwt-decode' import { jwtDecode } from 'jwt-decode'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import useQuery from './useQuery' import useQuery from './useQuery'
import { useStoreActions, useStoreState } from '../../store/store' import { store, useStoreActions, useStoreState } from '../../store/store'
import { import {
FailedSignInResponse, FailedSignInResponse,
SignInCredential, SignInCredential,
@ -13,7 +13,6 @@ import { isLoginSuccess } from '../../proxy/account/models'
import { REDIRECT_URL_KEY } from '../../constants/app.constant' import { REDIRECT_URL_KEY } from '../../constants/app.constant'
import appConfig from '../../proxy/configs/app.config' import appConfig from '../../proxy/configs/app.config'
import { register } from '../../services/account.service' import { register } from '../../services/account.service'
import { hasPendingChangelog } from '@/views/version/swRegistration'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
type Status = 'success' | 'failed' | 'error' type Status = 'success' | 'failed' | 'error'
@ -72,7 +71,11 @@ function useAuth() {
const redirectUrl = query.get(REDIRECT_URL_KEY) const redirectUrl = query.get(REDIRECT_URL_KEY)
// Uygulama yeni bir sürümle/deploy ile açıldıysa kullanıcı önce // 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. // 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 ? ROUTES_ENUM.protected.admin.changeLog
: (redirectUrl ?? appConfig.authenticatedEntryPath) : (redirectUrl ?? appConfig.authenticatedEntryPath)
navigate(target) navigate(target)

View file

@ -6,7 +6,8 @@ import React, { useEffect, useState } from 'react'
import PageTitle from '@/components/shared/PageTitle' import PageTitle from '@/components/shared/PageTitle'
import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa' import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa'
import { releaseNoteService, type ReleaseNoteDto } from '@/services/releaseNote.service' 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. // User dropdown reuses the exact update-check flow owned by this component.
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
@ -106,6 +107,7 @@ const Log = ({
const Changelog = () => { const Changelog = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
const { isCheckingUpdate, checkForUpdate } = useManualUpdateCheck() const { isCheckingUpdate, checkForUpdate } = useManualUpdateCheck()
const clearPendingChangelog = useStoreActions((actions) => actions.locale.clearPendingChangelog)
// Kaynak `ReleaseNote` entity'sidir; liste yalnızca bu ekran açıldığında // Kaynak `ReleaseNote` entity'sidir; liste yalnızca bu ekran açıldığında
// istenir (application-configuration sadece çalışan sürümü taşır). // istenir (application-configuration sadece çalışan sürümü taşır).
@ -120,7 +122,7 @@ const Changelog = () => {
.getList() .getList()
.then(setReleases) .then(setReleases)
.catch((error: unknown) => console.warn('Sürüm notları alınamadı.', error)) .catch((error: unknown) => console.warn('Sürüm notları alınamadı.', error))
}, []) }, [clearPendingChangelog])
return ( return (
<Container> <Container>

View file

@ -19,10 +19,11 @@ import './installPrompt'
* 2. Yeni worker bulunursa kurulum başlar; `src/sw.ts` indirdiği her dosyayı * 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. * isim/boyut/yüzde olarak yayınlar, bu modül state'e çevirir.
* 3. Kurulum bitince SKIP_WAITING `controllerchange` reload. * 3. Kurulum bitince SKIP_WAITING `controllerchange` reload.
* 4. Backend'den gelen sürüm son görülenden farklıysa changelog bayrağı * 4. Changelog yönlendirmesi bu modülün işi DEĞİLDİR: son görülen sürüm ve
* yazılır; login sonrası kullanıcı ChangeLog ekranına yönlendirilir. * bekleyen changelog bayrağı store'un `locale` diliminde tutulur
* Tetikleyici sürümdür, bundle değil: yalnızca yeni bir sürüm notu (git * (`setUiVersion` / `pendingChangelogVersion`), böylece durum tek yerde ve
* tag) eklendiğinde de kullanıcı değişiklikleri görür. * `[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 * Bu dosya entry chunk'ındadır: React/UI/store bağımlılığı yoktur, pencere
* yalnızca gerçekten indirme varken lazy yüklenir. * 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 MIN_CHECK_GAP = 30 * 1000
const ACTIVATION_RETRY_DELAY = 8_000 const ACTIVATION_RETRY_DELAY = 8_000
const ACTIVATION_TIMEOUT = 45_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 REGISTRATION_TIMEOUT = 10_000
const UPDATE_CALL_TIMEOUT = 20_000 const UPDATE_CALL_TIMEOUT = 20_000
/** Kurulum/aktivasyon başarısız olduğunda otomatik yeniden deneme süresi. */ /** Kurulum/aktivasyon başarısız olduğunda otomatik yeniden deneme süresi. */
const RETRY_DELAY_SECONDS = 15 const RETRY_DELAY_SECONDS = 15
const LAST_VERSION_KEY = 'sozsoft:last-version' /**
const PENDING_CHANGELOG_KEY = 'sozsoft:pending-changelog' * Chunk yeniden yükleme koruması: sonsuz reload döngüsünü keser.
const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload' *
* 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. */ /** 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' const isServiceWorkerEnabled = import.meta.env.PROD || import.meta.env.VITE_PWA_DEV === 'true'
@ -129,19 +138,18 @@ const withTimeout = <T>(promise: Promise<T>, ms: number) =>
new Promise<undefined>((resolve) => window.setTimeout(() => resolve(undefined), ms)), new Promise<undefined>((resolve) => window.setTimeout(() => resolve(undefined), ms)),
]) ])
const readStorage = (key: string, session = false) => { const readStorage = (key: string) => {
try { try {
return (session ? sessionStorage : localStorage).getItem(key) ?? undefined return localStorage.getItem(key) ?? undefined
} catch { } catch {
return undefined return undefined
} }
} }
const writeStorage = (key: string, value: string | undefined, session = false) => { const writeStorage = (key: string, value: string | undefined) => {
try { try {
const store = session ? sessionStorage : localStorage if (value === undefined) localStorage.removeItem(key)
if (value === undefined) store.removeItem(key) else localStorage.setItem(key, value)
else store.setItem(key, value)
} catch { } catch {
/* storage kapalıysa yoksay */ /* 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 çırır; ayrı bir sürüm isteği yoktur.
* Yük şekli: `extraProperties.changeLogs = { uiVersion, tags[] }`. * Yük şekli: `extraProperties.changeLogs = { uiVersion, tags[] }`.
*/ */
export const setAppVersionInfo = (raw: unknown) => { export const setAppVersionInfo = (raw: unknown) => {
@ -253,29 +265,6 @@ export const setAppVersionInfo = (raw: unknown) => {
currentVersion: data.uiVersion, currentVersion: data.uiVersion,
changeLog: Array.isArray(data.tags) ? data.tags.filter(Boolean) : [], 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 ı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 pollTimer: number | undefined
let activationRetryTimer: number | undefined let activationRetryTimer: number | undefined
let activationTimeoutTimer: number | undefined let activationTimeoutTimer: number | undefined
let activationHandoffTimer: number | undefined
const watchedRegistrations = new WeakSet<ServiceWorkerRegistration>() const watchedRegistrations = new WeakSet<ServiceWorkerRegistration>()
const watchedWorkers = new WeakSet<ServiceWorker>() const watchedWorkers = new WeakSet<ServiceWorker>()
@ -298,8 +288,39 @@ const watchedWorkers = new WeakSet<ServiceWorker>()
function clearActivationTimers() { function clearActivationTimers() {
window.clearTimeout(activationRetryTimer) window.clearTimeout(activationRetryTimer)
window.clearTimeout(activationTimeoutTimer) window.clearTimeout(activationTimeoutTimer)
window.clearTimeout(activationHandoffTimer)
activationRetryTimer = undefined activationRetryTimer = undefined
activationTimeoutTimer = 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 ı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 ı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() { function reloadForUpdate() {
@ -539,6 +560,12 @@ function handleWorkerMessage(event: MessageEvent) {
*/ */
const transitions = controlled && data.phase !== 'warmup' 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 } const totals = { loadedBytes: data.loadedBytes, totalBytes: data.totalBytes }
switch (data.type) { switch (data.type) {
@ -575,7 +602,8 @@ function handleWorkerMessage(event: MessageEvent) {
visible: true, visible: true,
percent: 100, percent: 100,
}) })
if (!transitions) hideDialogLater() if (transitions) scheduleActivationHandoff()
else hideDialogLater()
break break
case 'SW_PRECACHE_ERROR': case 'SW_PRECACHE_ERROR':
if (!transitions) { if (!transitions) {
@ -714,8 +742,14 @@ function scheduleWarmup(reg: ServiceWorkerRegistration) {
function trackInstallingWorker(reg: ServiceWorkerRegistration) { function trackInstallingWorker(reg: ServiceWorkerRegistration) {
const worker = reg.installing const worker = reg.installing
// Controller yoksa bu ilk kurulumdur; kullanıcıya güncelleme demek yanlış olur. if (!worker || watchedWorkers.has(worker)) return
if (!worker || !navigator.serviceWorker.controller || 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) watchedWorkers.add(worker)
void showUpdateDialog('downloading') void showUpdateDialog('downloading')
@ -776,8 +810,8 @@ function startWatchers() {
function handleChunkLoadFailures() { function handleChunkLoadFailures() {
window.addEventListener('vite:preloadError', (event) => { window.addEventListener('vite:preloadError', (event) => {
if (reloading) return if (reloading) return
if (readStorage(CHUNK_RELOAD_GUARD_KEY, true)) return if (readStorage(CHUNK_RELOAD_GUARD_KEY)) return
writeStorage(CHUNK_RELOAD_GUARD_KEY, '1', true) writeStorage(CHUNK_RELOAD_GUARD_KEY, '1')
event.preventDefault() event.preventDefault()
console.warn('Eksik chunk tespit edildi, sayfa yenileniyor.') console.warn('Eksik chunk tespit edildi, sayfa yenileniyor.')
reloading = true reloading = true
@ -785,7 +819,7 @@ function handleChunkLoadFailures() {
}) })
// Sayfa sorunsuz açıldıysa koruma bayrağını temizle. // 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. */ /** Kapalıyken önceki oturumlardan kalan kayıtları temizler. */