Update new version checking and Report Categories

This commit is contained in:
Sedat ÖZTÜRK 2026-07-14 10:37:13 +03:00
parent 1d05d24e41
commit 28ec641275
13 changed files with 413 additions and 80 deletions

View file

@ -19949,6 +19949,72 @@
"key": "App.Tenant.Create",
"en": "Tenant Create",
"tr": "Tenant Oluştur"
},
{
"resourceName": "Platform",
"key": "App.CheckUpdate",
"en": "Checking...",
"tr": "Kontrol ediliyor..."
},
{
"resourceName": "Platform",
"key": "App.CheckForUpdates",
"en": "Check for Updates",
"tr": "Güncellemeyi Kontrol Et"
},
{
"resourceName": "Platform",
"key": "App.NotSupportServiceWorkerUpdate",
"en": "Your browser does not support service worker updates.",
"tr": "Tarayıcınız service worker güncellemelerini desteklemiyor."
},
{
"resourceName": "Platform",
"key": "App.NewVersionAvailable",
"en": "A new service worker has been found. The application will automatically update when the files are ready.",
"tr": "Dosyalar hazırlandığında uygulama otomatik olarak güncellenecek."
},
{
"resourceName": "Platform",
"key": "App.ManualUpdateFailed",
"en": "Please check your connection and try again.",
"tr": "Lütfen bağlantınızı kontrol edip tekrar deneyin."
},
{
"resourceName": "Platform",
"key": "App.ChangeLogDescription",
"en": "Check if there's a new application version available on the server.",
"tr": "Sunucuda yeni bir uygulama sürümü olup olmadığını kontrol edin."
},
{
"resourceName": "Platform",
"key": "App.NoNewVersionFound",
"en": "No new version was found available.",
"tr": "Kullanılabilir yeni bir sürüm bulunamadı."
},
{
"resourceName": "Platform",
"key": "App.UpToDate",
"en": "The app is up to date.",
"tr": "Uygulama güncel"
},
{
"resourceName": "Platform",
"key": "App.UpdateCouldNotBeChecked",
"en": "Update could not be checked",
"tr": "Güncelleme kontrol edilemedi"
},
{
"resourceName": "Platform",
"key": "App.NewVersionFound",
"en": "New version found.",
"tr": "Yeni sürüm bulundu"
},
{
"resourceName": "Platform",
"key": "App.UpdateCheckFailed",
"en": "Update check failed.",
"tr": "Güncelleme kontrolü başarısız"
}
]
}

View file

@ -1417,7 +1417,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportCategory)),
KeyFieldName = "Id",
DefaultFilter = DefaultFilterJson,
KeyFieldDbSourceType = DbType.Int32,
KeyFieldDbSourceType = DbType.Guid,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson(),
HeaderFilterJson = DefaultHeaderFilterJson(),
@ -1439,8 +1439,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
]}
}),
DeleteCommand = $"UPDATE \"{FullNameTable(TableNameEnum.ReportCategory)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
});
#region Report Categories Fields
@ -1582,7 +1582,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
AuthName = listFormName,
Url=$"/admin/reports/DynamicReport/view/@Id",
IsVisible = true,
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Raporlar')"
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Raporlar')"
},
new() {
Hint = "App.Platform.Design",
@ -1591,7 +1591,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
AuthName = listFormName + ".Update",
Url=$"/admin/reports/DynamicReport/design/@Id",
IsVisible = true,
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Raporlar')"
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Raporlar')"
},
new() {
Hint = "App.Platform.View",
@ -1600,7 +1600,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
AuthName = listFormName,
Url=$"/admin/reports/DynamicGrid/view/@Name",
IsVisible = true,
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Listeler')"
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Listeler')"
},
new() {
Hint = "App.Platform.View",
@ -1609,7 +1609,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
AuthName = listFormName,
Url=$"/admin/reports/DynamicGrid/view/@Id/@Name",
IsVisible = true,
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Formlar')"
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Formlar')"
},
}),
}
@ -1660,7 +1660,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
SourceDbType = DbType.Guid,
FieldName = "CategoryId",
CaptionName = "App.Listform.ListformField.CategoryId",
Width = 0,

View file

@ -5,7 +5,7 @@ using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities
{
public class ReportCategory : FullAuditedEntity<string>, IMultiTenant
public class ReportCategory : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
@ -15,9 +15,12 @@ namespace Sozsoft.Platform.Entities
public ICollection<ReportTemplate> ReportTemplates { get; set; }
public ReportCategory(string id)
protected ReportCategory()
{
}
public ReportCategory(Guid id) : base(id)
{
Id = id;
}
}
}

View file

@ -12,7 +12,7 @@ namespace Sozsoft.Platform.Entities
public string Description { get; set; }
public string HtmlContent { get; set; }
public string CategoryId { get; set; }
public Guid CategoryId { get; set; }
public ReportCategory ReportCategory { get; set; }
public string Status { get; set; }
@ -23,7 +23,7 @@ namespace Sozsoft.Platform.Entities
string name,
string description,
string htmlContent,
string categoryId,
Guid categoryId,
string status
) : base(id)
{

View file

@ -660,7 +660,6 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportCategory)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Id).HasMaxLength(256);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.Property(x => x.Description).HasMaxLength(4096); // JSON string
b.Property(x => x.Icon).HasMaxLength(64);
@ -674,7 +673,6 @@ public class PlatformDbContext :
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.Property(x => x.CategoryId).HasMaxLength(256);
b.Property(x => x.Description).HasMaxLength(1024);
b.Property(x => x.HtmlContent).IsRequired();
b.Property(x => x.Status).HasMaxLength(20);

View file

@ -4482,9 +4482,8 @@ namespace Sozsoft.Platform.Migrations
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -4547,9 +4546,8 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("CategoryId")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")

View file

@ -650,7 +650,7 @@ namespace Sozsoft.Platform.Migrations
name: "Adm_T_ReportCategory",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: true),
@ -2259,7 +2259,7 @@ namespace Sozsoft.Platform.Migrations
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Description = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
HtmlContent = table.Column<string>(type: "nvarchar(max)", nullable: false),
CategoryId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),

View file

@ -4479,9 +4479,8 @@ namespace Sozsoft.Platform.Migrations
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
{
b.Property<string>("Id")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -4544,9 +4543,8 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("CategoryId")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")

View file

@ -491,7 +491,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<GlobalSearch, int> _globalSearch;
private readonly IRepository<CustomEndpoint, Guid> _customEndpointRepository;
private readonly IRepository<CustomComponent, Guid> _customComponentRepository;
private readonly IRepository<ReportCategory, string> _reportCategoriesRepository;
private readonly IRepository<ReportCategory, Guid> _reportCategoriesRepository;
private readonly IRepository<ReportTemplate, Guid> _reportTemplatesRepository;
private readonly IRepository<Home, Guid> _homeRepository;
private readonly IRepository<About, Guid> _aboutRepository;
@ -548,7 +548,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<SkillLevel, string> skillLevelRepository,
IRepository<CustomEndpoint, Guid> customEndpointRepository,
IRepository<CustomComponent, Guid> customComponentRepository,
IRepository<ReportCategory, string> reportCategoriesRepository,
IRepository<ReportCategory, Guid> reportCategoriesRepository,
IRepository<ReportTemplate, Guid> reportTemplatesRepository,
IRepository<Home, Guid> homeRepository,
IRepository<About, Guid> aboutRepository,
@ -867,7 +867,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
if (!exists)
{
await _reportCategoriesRepository.InsertAsync(new ReportCategory(item.Name)
await _reportCategoriesRepository.InsertAsync(new ReportCategory(Guid.NewGuid())
{
Name = item.Name,
Description = item.Description,

View file

@ -1,6 +1,18 @@
{
"commit": "e7be51a",
"commit": "1d05d24",
"releases": [
{
"version": "1.1.07",
"buildDate": "2026-07-13",
"commit": "50cb3eaf979eb5027172a92a4e941ed64aa3671e",
"changeLog": [
"- localStorage tüm bilgiler easy-peasy içerisinde birleştirildi.",
"- Codex 5.6 sol optimizasyon yapıldı. Bir çok komponent Lazy load ile yüklendi.",
"- Workflow komponentinden birden fazla kişi onay verebilir.",
"- Posta Kodu eklendi.",
"- Sales Order kısmında Yeni ve Eski müşteri sipariş ayrımı yapıldı"
]
},
{
"version": "1.1.06",
"buildDate": "2026-07-06",

View file

@ -1,10 +1,12 @@
import React, { useEffect, useState } from "react"
import AdaptableCard from "@/components/shared/AdaptableCard"
import Container from "@/components/shared/Container"
import { FaTag, FaRegCheckCircle } from "react-icons/fa"
import { useLocalization } from "@/utils/hooks/useLocalization"
import { Helmet } from "react-helmet"
import { APP_NAME } from "@/constants/app.constant"
import AdaptableCard from '@/components/shared/AdaptableCard'
import Container from '@/components/shared/Container'
import { Button, Notification, toast } from '@/components/ui'
import { APP_NAME } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa'
import { checkForAppUpdate } from './swRegistration'
type Release = {
version: string
@ -12,7 +14,15 @@ type Release = {
changeLog: string[]
}
const Log = ({ version, date, children }: { version: string; date: string; children?: React.ReactNode }) => {
const Log = ({
version,
date,
children,
}: {
version: string
date: string
children?: React.ReactNode
}) => {
return (
<div className="relative pl-4 sm:pl-32 py-4 group">
<div className="flex flex-col sm:flex-row items-start mb-1 group-last:before:hidden before:absolute before:left-2 sm:before:left-0 before:h-full before:px-px before:bg-slate-200 sm:before:ml-[6.5rem] before:self-start before:-translate-x-1/2 before:translate-y-3 after:absolute after:left-2 sm:after:left-0 after:w-2 after:h-2 after:bg-indigo-600 after:border-4 after:box-content after:border-slate-50 after:rounded-full sm:after:ml-[6.5rem] after:-translate-x-1/2 after:translate-y-1.5">
@ -20,8 +30,7 @@ const Log = ({ version, date, children }: { version: string; date: string; child
{date}
</time>
<div className="flex items-center text-xl font-bold text-gray-900 dark:text-white">
<FaTag className="mr-2 text-indigo-500" />
v{version}
<FaTag className="mr-2 text-indigo-500" />v{version}
</div>
</div>
<div className="text-slate-500">{children}</div>
@ -32,9 +41,10 @@ const Log = ({ version, date, children }: { version: string; date: string; child
const Changelog = () => {
const { translate } = useLocalization()
const [releases, setReleases] = useState<Release[]>([])
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
useEffect(() => {
fetch("/version.json?ts=" + Date.now())
fetch('/version.json?ts=' + Date.now())
.then((res) => res.json())
.then((data) => {
if (data?.releases) {
@ -44,15 +54,73 @@ const Changelog = () => {
.catch(() => setReleases([]))
}, [])
const handleManualUpdate = async () => {
if (isCheckingUpdate) return
setIsCheckingUpdate(true)
try {
const result = await checkForAppUpdate()
if (result === 'up-to-date') {
toast.push(
<Notification title={translate('::App.UpToDate')} type="success">
{translate('::App.NoNewVersionFound')}
</Notification>,
{ placement: 'bottom-end' },
)
} else if (result === 'unsupported') {
toast.push(
<Notification title={translate('::App.UpdateCouldNotBeChecked')} type="warning">
{translate('::App.NotSupportServiceWorkerUpdate')}
</Notification>,
{ placement: 'bottom-end' },
)
} else {
toast.push(
<Notification title={translate('::App.NewVersionFound')} type="info">
{translate('::App.NewVersionAvailable')}
</Notification>,
{ placement: 'bottom-end' },
)
}
} catch (error) {
console.error('Manual application update failed.', error)
toast.push(
<Notification title={translate('::App.UpdateCheckFailed')} type="danger">
{translate('::App.ManualUpdateFailed')}
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setIsCheckingUpdate(false)
}
}
return (
<Container>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate("::App.ChangeLog")}
title={translate('::App.ChangeLog')}
defaultTitle={APP_NAME}
/>
<AdaptableCard>
<div className="p-2">
<div className="mb-6 flex flex-col gap-3 border-b border-gray-200 pb-5 dark:border-gray-700 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 className="mb-1">{translate('::App.ChangeLog')}</h3>
<p className="text-sm text-gray-500">{translate('::App.ChangeLogDescription')}</p>
</div>
<Button
icon={<FaSyncAlt />}
loading={isCheckingUpdate}
variant="solid"
onClick={handleManualUpdate}
>
{isCheckingUpdate
? translate('::App.CheckUpdate')
: translate('::App.CheckForUpdates')}
</Button>
</div>
<div className="space-y-1">
{releases.map((rel) => (
<Log key={rel.version} version={rel.version} date={rel.buildDate}>

View file

@ -1,5 +1,20 @@
import { registerSW } from 'virtual:pwa-register'
const ACTIVATION_RETRY_DELAY = 5_000
const ACTIVATION_TIMEOUT = 30_000
let registrationStarted = false
let activationInProgress = false
let reloadStarted = false
let currentRegistration: ServiceWorkerRegistration | undefined
let activateUpdateHandler: ((reloadPage?: boolean) => Promise<void>) | undefined
let registrationInitialization: Promise<void> | undefined
let activationRetryTimer: number | undefined
let activationTimeoutTimer: number | undefined
const watchedInstallingWorkers = new WeakSet<ServiceWorker>()
export type ManualUpdateResult = 'up-to-date' | 'updating' | 'unsupported'
function showUpdateOverlay() {
if (document.getElementById('sw-update-overlay')) return
@ -67,30 +82,202 @@ function showUpdateOverlay() {
document.body.appendChild(overlay)
}
export const registerServiceWorker = () => {
// autoUpdate modunda onNeedRefresh tetiklenmez.
// updatefound → yeni SW installing → overlay aç.
// skipWaiting: true (workbox config) → controllerchange → autoUpdate reload.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
// ready'den önce updatefound zaten tetiklendiyse (race condition)
if (registration.installing && navigator.serviceWorker.controller) {
showUpdateOverlay()
}
function hideUpdateOverlay() {
document.getElementById('sw-update-overlay')?.remove()
document.getElementById('sw-update-overlay-style')?.remove()
}
registration.addEventListener('updatefound', () => {
// Güncelleme başlar başlamaz overlay'i göster
if (navigator.serviceWorker.controller) {
showUpdateOverlay()
}
})
})
function clearActivationTimers() {
window.clearTimeout(activationRetryTimer)
window.clearTimeout(activationTimeoutTimer)
activationRetryTimer = undefined
activationTimeoutTimer = undefined
}
function reloadForUpdate() {
if (reloadStarted) return
reloadStarted = true
clearActivationTimers()
window.location.reload()
}
function activateWaitingWorker(
registration: ServiceWorkerRegistration | undefined,
activateUpdate: (reloadPage?: boolean) => Promise<void>,
showOverlay: boolean,
) {
if (activationInProgress) {
if (showOverlay) showUpdateOverlay()
return
}
activationInProgress = true
if (showOverlay) {
showUpdateOverlay()
}
registerSW({
immediate: true,
onOfflineReady() {
console.log('📦 App offline ready')
},
// Workbox'ın controlling olayı herhangi bir nedenle kaçarsa native olay da
// yeni worker kontrolü aldığı anda sayfayı yeniler.
navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, {
once: true,
})
// Workbox callback sırasına bağlı kalmadan waiting worker'ı doğrudan geçir.
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
void activateUpdate(true).catch((error: unknown) => {
console.error('Service worker activation failed.', error)
activationInProgress = false
clearActivationTimers()
hideUpdateOverlay()
})
// Bazı tarayıcılarda Workbox mesajı bekleyen worker'a ulaşmayabiliyor.
// Kısa bir süre sonra native mesajla bir kez daha aktivasyon istenir.
activationRetryTimer = window.setTimeout(() => {
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
}, ACTIVATION_RETRY_DELAY)
// Aktivasyon yine de tamamlanamazsa uygulamayı sonsuza kadar kilitleme.
// Eski worker çalışmaya devam eder; bir sonraki açılışta tekrar denenir.
activationTimeoutTimer = window.setTimeout(() => {
activationInProgress = false
clearActivationTimers()
// Worker waiting durumundan çıktıysa aktivasyon gerçekleşmiş fakat
// controllerchange olayı kaçmış olabilir. Yeni precache'i almak için yenile.
if (!registration?.waiting && registration?.active) {
reloadForUpdate()
return
}
hideUpdateOverlay()
console.warn('Service worker activation timed out; continuing with current version.')
}, ACTIVATION_TIMEOUT)
}
function showUpdateWhileInstalling(registration: ServiceWorkerRegistration) {
const installingWorker = registration.installing
if (
!installingWorker ||
!navigator.serviceWorker.controller ||
watchedInstallingWorkers.has(installingWorker)
) {
return
}
watchedInstallingWorkers.add(installingWorker)
showUpdateOverlay()
installingWorker.addEventListener('statechange', () => {
// Precache dosyalarından biri indirilemezse worker redundant olur. Eski
// uygulama çalışmaya devam eder ve kullanıcı kilitli ekranda bırakılmaz.
if (installingWorker.state === 'redundant' && !activationInProgress) {
hideUpdateOverlay()
}
})
}
function watchForUpdate(registration: ServiceWorkerRegistration) {
registration.addEventListener('updatefound', () => {
// Gerçek worker güncellemesi bulunduğu anda, precache indirmesi başlamadan
// Loading'i göster ve aktivasyon/reload tamamlanana kadar açık tut.
showUpdateWhileInstalling(registration)
})
// Listener bağlanmadan hemen önce başlamış bir kurulumu da kaçırma.
showUpdateWhileInstalling(registration)
}
function checkForUpdate(registration: ServiceWorkerRegistration) {
// Bir deploy versiyon numarası/tag değişmeden yapılsa bile sw.js içindeki
// precache manifest'i değişir. Uygulama açılırken yapılan bu tek kontrol
// yeni build'i algılar; açık uygulamada periyodik bir arka plan işi çalışmaz.
void registration.update().catch((error: unknown) => {
console.warn('Service worker update check failed.', error)
})
}
export const registerServiceWorker = () => {
if (registrationStarted || !('serviceWorker' in navigator)) return
registrationStarted = true
registrationInitialization = navigator.serviceWorker
.getRegistration()
.catch(() => undefined)
.then((existingRegistration) => {
if (existingRegistration) {
watchForUpdate(existingRegistration)
}
const activateUpdate = registerSW({
immediate: true,
onRegisteredSW(_swUrl, registration) {
if (!registration) return
if (registration !== existingRegistration) {
watchForUpdate(registration)
}
currentRegistration = registration
checkForUpdate(registration)
// waiting yalnızca aktif worker'dan farklı, kurulumu tamamlanmış gerçek
// bir worker olduğunda bulunur. Onu doğrudan etkinleştir.
if (registration.waiting && navigator.serviceWorker.controller) {
activateWaitingWorker(registration, activateUpdate, true)
}
},
onNeedRefresh() {
// Bu callback worker'ın bütün yeni JS/CSS dosyalarını başarıyla precache'e
// almasından sonra çalışır; aktivasyon ve reload atomik yapılır.
activateWaitingWorker(currentRegistration, activateUpdate, true)
},
onNeedReload() {
reloadForUpdate()
},
onOfflineReady() {
console.log('📦 App offline ready')
},
onRegisterError(error) {
activationInProgress = false
clearActivationTimers()
hideUpdateOverlay()
console.error('Service worker registration failed.', error)
},
})
activateUpdateHandler = activateUpdate
})
}
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
if (!('serviceWorker' in navigator)) return 'unsupported'
// App içindeki normal kayıt işlemi henüz bitmediyse önce onu tamamla.
await registrationInitialization
const registration =
currentRegistration ?? (await navigator.serviceWorker.ready.catch(() => undefined))
if (!registration) return 'unsupported'
let updateFound = false
const handleUpdateFound = () => {
updateFound = true
}
registration.addEventListener('updatefound', handleUpdateFound)
try {
await registration.update()
} finally {
registration.removeEventListener('updatefound', handleUpdateFound)
}
const updateAvailable = Boolean(updateFound || registration.installing || registration.waiting)
if (!updateAvailable) return 'up-to-date'
if (registration.waiting && activateUpdateHandler) {
activateWaitingWorker(registration, activateUpdateHandler, true)
}
// installing durumundaysa gerçek onNeedRefresh callback'i indirme bittikten
// sonra aynı overlay/aktivasyon akışını başlatır.
return 'updating'
}

View file

@ -17,10 +17,11 @@ export default defineConfig(async ({ mode }) => {
plugins: [
react(),
VitePWA({
// Deploy'dan sonra otomatik güncelle
registerType: 'autoUpdate',
// Kayıt kodunu otomatik enjekte et (virtual:pwa-register yazmadan da çalışır)
injectRegister: 'auto',
// Yeni sürüm önce tamamen indirilir, ardından uygulama kontrollü olarak
// yeni service worker'a geçer. Böylece eski ekran ile yeni chunk'lar karışmaz.
registerType: 'prompt',
// Kayıt ve loading akışı src/views/version/swRegistration.ts tarafından yönetilir.
injectRegister: false,
// Dev ortamında SW'yi aç, prod'da kapalı tut (build edilmiş SW prod'da zaten aktif olur)
devOptions: {
enabled: mode !== 'production',
@ -38,9 +39,10 @@ export default defineConfig(async ({ mode }) => {
// Büyük asset'leri de cache'leyebil
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
// EN KRİTİK: yeni SW beklemeden kontrolü alsın
// Yeni worker tüm precache dosyalarını indirdikten sonra uygulama
// SKIP_WAITING mesajını gönderir. clientsClaim açık sekmeleri o anda yeniler.
clientsClaim: true,
skipWaiting: true,
skipWaiting: false,
// Eski workbox cache'lerini temizle
cleanupOutdatedCaches: true,
@ -48,14 +50,14 @@ export default defineConfig(async ({ mode }) => {
// SPA fallback'i API çağrılarına uygulama
navigateFallbackDenylist: [/^\/api\//],
// ⭐⭐ BU KISMI EKLEYİN: Cache sorununu çözecek runtime caching
runtimeCaching: [
{
urlPattern: /\.(?:js|css|wasm)$/,
handler: 'NetworkFirst',
// Hash'li build dosyaları immutable'dır. Normal kullanımda ağ
// beklenmez; deploy güncellemesini precache manifest'i yönetir.
handler: 'CacheFirst',
options: {
cacheName: 'static-resources-v2',
networkTimeoutSeconds: 5,
expiration: {
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,
@ -66,13 +68,14 @@ export default defineConfig(async ({ mode }) => {
},
},
{
urlPattern: /\.json$/,
// Changelog/versiyon bilgisi cache yüzünden eski kalmamalı.
urlPattern: /\/version\.json$/,
handler: 'NetworkFirst',
options: {
cacheName: 'json-resources-v1',
networkTimeoutSeconds: 5,
cacheName: 'app-version-v1',
networkTimeoutSeconds: 3,
expiration: {
maxEntries: 50,
maxEntries: 5,
maxAgeSeconds: 24 * 60 * 60,
},
cacheableResponse: {