ReleaseNotes düzeltmesi
This commit is contained in:
parent
ee121db521
commit
cced417ede
10 changed files with 194 additions and 68 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)"' \
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 çağı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 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 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 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() {
|
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. */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue