diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 45d2d9b..b4192b2 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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" } ] } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 6be4b90..f1a9a67 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -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, diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportCategory.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportCategory.cs index a2f2ff7..9f399ba 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportCategory.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportCategory.cs @@ -5,7 +5,7 @@ using Volo.Abp.MultiTenancy; namespace Sozsoft.Platform.Entities { - public class ReportCategory : FullAuditedEntity, IMultiTenant + public class ReportCategory : FullAuditedEntity, IMultiTenant { public Guid? TenantId { get; set; } @@ -15,9 +15,12 @@ namespace Sozsoft.Platform.Entities public ICollection ReportTemplates { get; set; } - public ReportCategory(string id) + protected ReportCategory() + { + } + + public ReportCategory(Guid id) : base(id) { - Id = id; } } } diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportTemplate.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportTemplate.cs index 6031383..4d54680 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportTemplate.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/Report/ReportTemplate.cs @@ -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) { diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index f326c15..57383ba 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -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); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.Designer.cs index d8cf562..8bedfd3 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.Designer.cs @@ -4482,9 +4482,8 @@ namespace Sozsoft.Platform.Migrations modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b => { - b.Property("Id") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("Id") + .HasColumnType("uniqueidentifier"); b.Property("CreationTime") .HasColumnType("datetime2") @@ -4547,9 +4546,8 @@ namespace Sozsoft.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); b.Property("CreationTime") .HasColumnType("datetime2") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.cs index e81a833..d062a0d 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260713103339_Initial.cs @@ -650,7 +650,7 @@ namespace Sozsoft.Platform.Migrations name: "Adm_T_ReportCategory", columns: table => new { - Id = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Id = table.Column(type: "uniqueidentifier", nullable: false), TenantId = table.Column(type: "uniqueidentifier", nullable: true), Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), Description = table.Column(type: "nvarchar(max)", maxLength: 4096, nullable: true), @@ -2259,7 +2259,7 @@ namespace Sozsoft.Platform.Migrations Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), Description = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: true), HtmlContent = table.Column(type: "nvarchar(max)", nullable: false), - CategoryId = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + CategoryId = table.Column(type: "uniqueidentifier", nullable: false), Status = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), CreatorId = table.Column(type: "uniqueidentifier", nullable: true), diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index c01af7f..05a6e2a 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -4479,9 +4479,8 @@ namespace Sozsoft.Platform.Migrations modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b => { - b.Property("Id") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("Id") + .HasColumnType("uniqueidentifier"); b.Property("CreationTime") .HasColumnType("datetime2") @@ -4544,9 +4543,8 @@ namespace Sozsoft.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); b.Property("CreationTime") .HasColumnType("datetime2") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs index cdeb566..36009cb 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs @@ -491,7 +491,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency private readonly IRepository _globalSearch; private readonly IRepository _customEndpointRepository; private readonly IRepository _customComponentRepository; - private readonly IRepository _reportCategoriesRepository; + private readonly IRepository _reportCategoriesRepository; private readonly IRepository _reportTemplatesRepository; private readonly IRepository _homeRepository; private readonly IRepository _aboutRepository; @@ -548,7 +548,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency IRepository skillLevelRepository, IRepository customEndpointRepository, IRepository customComponentRepository, - IRepository reportCategoriesRepository, + IRepository reportCategoriesRepository, IRepository reportTemplatesRepository, IRepository homeRepository, IRepository 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, diff --git a/ui/public/version.json b/ui/public/version.json index a823ebd..2d7a0c8 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -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", diff --git a/ui/src/views/version/ChangeLog.tsx b/ui/src/views/version/ChangeLog.tsx index bd94323..3b052e7 100644 --- a/ui/src/views/version/ChangeLog.tsx +++ b/ui/src/views/version/ChangeLog.tsx @@ -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 (
@@ -20,8 +30,7 @@ const Log = ({ version, date, children }: { version: string; date: string; child {date}
- - v{version} + v{version}
{children}
@@ -32,9 +41,10 @@ const Log = ({ version, date, children }: { version: string; date: string; child const Changelog = () => { const { translate } = useLocalization() const [releases, setReleases] = useState([]) + 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( + + {translate('::App.NoNewVersionFound')} + , + { placement: 'bottom-end' }, + ) + } else if (result === 'unsupported') { + toast.push( + + {translate('::App.NotSupportServiceWorkerUpdate')} + , + { placement: 'bottom-end' }, + ) + } else { + toast.push( + + {translate('::App.NewVersionAvailable')} + , + { placement: 'bottom-end' }, + ) + } + } catch (error) { + console.error('Manual application update failed.', error) + toast.push( + + {translate('::App.ManualUpdateFailed')} + , + { placement: 'bottom-end' }, + ) + } finally { + setIsCheckingUpdate(false) + } + } + return (
+
+
+

{translate('::App.ChangeLog')}

+

{translate('::App.ChangeLogDescription')}

+
+ +
{releases.map((rel) => ( diff --git a/ui/src/views/version/swRegistration.ts b/ui/src/views/version/swRegistration.ts index 6513792..00f78ba 100644 --- a/ui/src/views/version/swRegistration.ts +++ b/ui/src/views/version/swRegistration.ts @@ -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) | undefined +let registrationInitialization: Promise | undefined +let activationRetryTimer: number | undefined +let activationTimeoutTimer: number | undefined +const watchedInstallingWorkers = new WeakSet() + +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, + 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 => { + 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' +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 62cfbad..37644dd 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -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: {