ReleaseNoteAppService güncellemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-08-13 00:26:36 +03:00
parent 534dcce6da
commit 0abf181a9c
35 changed files with 1012 additions and 704 deletions

View file

@ -693,9 +693,27 @@ konfigürasyonu ile üretilir.
ve görseller runtime cache'tedir. ve görseller runtime cache'tedir.
- `registerType: 'prompt'` — yeni sürüm indirilir, indirme bitince otomatik devreye alınır - `registerType: 'prompt'` — yeni sürüm indirilir, indirme bitince otomatik devreye alınır
(`views/version/swRegistration.ts`, `sw.ts`, `AppUpdateDialog.tsx`). (`views/version/swRegistration.ts`, `sw.ts`, `AppUpdateDialog.tsx`).
- `version.json` her zaman ağdan tazelenir (NetworkFirst); changelog `/admin/changeLog` - Isıtma yalnızca kuruluma bağlı değildir: uygulama açılışta `ENSURE_WARMUP` mesajı yollar,
ekranında gösterilir, yeni deploy sonrası ilk girişte oraya yönlendirilir. service worker eksik kalan bileşenleri kaldığı yerden tamamlar.
- `scripts/generate-version.js` build sırasında sürüm bilgisini üretir. - **Sürüm bilgisi backend'dedir.** `Sas_H_ReleaseNote` tablosu tek kaynaktır; istemci sürüm
numarasını ve son sürümün notlarını `application-configuration` yanıtındaki
`extraProperties.changeLogs` alanından okur (`PlatformApplicationConfigurationContributor`),
tam listeyi ise `/api/app/release-note` endpoint'inden alır. Ayrı bir `version.json` isteği
yoktur. Changelog `/admin/changeLog` ekranında gösterilir; sürüm numarası son görülenden
farklıysa kullanıcı giriş sonrası oraya yönlendirilir (tetikleyici bundle değil, sürümdür —
yalnızca yeni bir git tag'i eklenmiş olsa da çalışır).
- **Sürüm notu akışı — DbMigrator gerektirmez:** `api/scripts/generate-release-notes.sh`
build sırasında (`build/api.sh`) git tag'lerinden `HttpApi.Host/release-notes.json` üretir,
dosya API imajına gömülür ve `ReleaseNoteStartupService` uygulama açılışında tabloyu bu
dosyayla eşitler. Yeni sürüm için tag atıp API'yi deploy etmek yeterlidir. Notlar
**annotated** tag mesajından okunur (`git tag -a 1.2.0 -m "..."`), her satır bir madde.
- **Tablo git tag listesinin aynasıdır:** yeni tag eklenir, mesajı düzeltilen tag güncellenir,
git'ten silinen tag tablodan da (hard delete ile) kaldırılır. Bu yüzden `Sas_H_ReleaseNote`
elle düzenlenmemelidir — ilk deploy'da geri alınır. Dosya okunamaz veya boşsa senkronizasyon
hiçbir şey yapmaz, tablo boşaltılmaz.
- Deploy algılaması service worker'ın kendi `registration.update()` kontrolüne dayanır:
herhangi bir chunk değiştiğinde entry hash'i, dolayısıyla index.html ve `sw.js` içindeki
precache manifest'i de değişir.
--- ---

View file

@ -0,0 +1,86 @@
#!/bin/bash
set -e
# Sürüm notlarını git tag'lerinden üretir:
# api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json
#
# Bu dosya API imajına gömülür; uygulama açılışında ReleaseNoteStartupService onu
# `Sas_H_ReleaseNote` tablosuna senkronize eder (yeni tag eklenir, mesajı değişen
# tag güncellenir). Yani YENİ SÜRÜM İÇİN DbMigrator ÇALIŞTIRMAK GEREKMEZ; API'yi
# deploy etmek yeterlidir. İstemci sürüm numarasını ve changelog'u
# application-configuration yanıtındaki `extraProperties.changeLogs` alanından okur.
#
# Deploy akışında build/api.sh tarafından otomatik çağrılır; elle de
# çalıştırılabilir. Git deposu gerektiği için imajın içinde değil, imaj
# oluşturulmadan ÖNCE build sunucusunda koşar.
#
# Dış bağımlılığı yoktur (jq/node gerekmez): JSON kaçışları sed ile yapılır.
cd "$(dirname "$0")/.."
OUTPUT="src/Sozsoft.Platform.HttpApi.Host/release-notes.json"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "> git deposu yok, ${OUTPUT} güncellenmedi." >&2
exit 0
fi
echo "> release-notes.json oluşturuluyor..."
# origin'de olmayan yerel tag'ler sürüm sırasını bozar; temizle. `--force` şart:
# yerel bir tag origin'dekinden farklı commit'e bakıyorsa git onu "would clobber
# existing tag" diyerek reddediyor ve YENİ tag'ler de alınmamış oluyor.
git fetch --prune --prune-tags --tags --force origin >/dev/null 2>&1 || true
# Sıra tag adına göre (creatordate değil): 1.1.10 > 1.1.09.
TAGS=$(git tag --list --sort=-v:refname)
if [ -z "$TAGS" ]; then
echo "> Tag bulunamadı, mevcut dosya korundu." >&2
exit 0
fi
# JSON string kaçışı: önce ters bölü, sonra tırnak, sonra sekme/CR.
escape_json() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' -e 's/\r//g'
}
TMP=$(mktemp)
printf '{\n "ReleaseNotes": [\n' > "$TMP"
FIRST_TAG=true
for TAG in $TAGS; do
VERSION=${TAG#v}
RELEASE_DATE=$(git log -1 --format=%ad --date=short "$TAG")
COMMIT=$(git rev-list -n 1 "$TAG")
if [ "$FIRST_TAG" = true ]; then FIRST_TAG=false; else printf ',\n' >> "$TMP"; fi
printf ' {\n' >> "$TMP"
printf ' "version": "%s",\n' "$(escape_json "$VERSION")" >> "$TMP"
printf ' "releaseDate": "%s",\n' "$RELEASE_DATE" >> "$TMP"
printf ' "commit": "%s",\n' "$COMMIT" >> "$TMP"
printf ' "notes": [' >> "$TMP"
# Annotated tag mesajı; her satır bir madde. Boş satırlar ve baştaki "-" / "*"
# işaretleri temizlenir. Lightweight tag'de mesaj boştur → notes: [].
FIRST_NOTE=true
while IFS= read -r LINE; do
[ -z "$LINE" ] && continue
if [ "$FIRST_NOTE" = true ]; then FIRST_NOTE=false; printf '\n' >> "$TMP"; else printf ',\n' >> "$TMP"; fi
printf ' "%s"' "$(escape_json "$LINE")" >> "$TMP"
done < <(git tag -l --format='%(contents)' "$TAG" \
| sed -e 's/\r//' -e 's/^[[:space:]]*[-*][[:space:]]*//' -e 's/[[:space:]]*$//' \
| grep -v '^$' || true)
if [ "$FIRST_NOTE" = true ]; then
printf ']\n' >> "$TMP"
else
printf '\n ]\n' >> "$TMP"
fi
printf ' }' >> "$TMP"
done
printf '\n ]\n}\n' >> "$TMP"
mv "$TMP" "$OUTPUT"
echo "> ${OUTPUT}: $(grep -c '"version":' "$OUTPUT") sürüm"

View file

@ -0,0 +1,14 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ReleaseNotes;
public class ReleaseNoteDto : EntityDto<Guid>
{
public string Version { get; set; }
public DateTime ReleaseDate { get; set; }
public string Commit { get; set; }
/// <summary>Sürüm notu maddeleri (entity'de satır satır saklanır).</summary>
public string[] ChangeLog { get; set; }
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.ReleaseNotes;
/// <summary>
/// Sürüm notları (ChangeLog ekranının kaynağı).
///
/// Kayıtlar git tag'lerinden üretilip <c>Sas_H_ReleaseNote</c> tablosuna
/// tohumlanır. Uygulamanın çalışan sürüm numarası ise application-configuration
/// yanıtındaki <c>extraProperties.changeLogs</c> ile gelir; bu servis yalnızca
/// tam listeyi döner, böylece her config isteğinde tüm changelog taşınmaz.
/// </summary>
[Authorize]
public class ReleaseNoteAppService : PlatformAppService
{
private readonly IRepository<ReleaseNote, Guid> _releaseNoteRepository;
public ReleaseNoteAppService(IRepository<ReleaseNote, Guid> releaseNoteRepository)
{
_releaseNoteRepository = releaseNoteRepository;
}
public async Task<List<ReleaseNoteDto>> GetListAsync()
{
var items = await _releaseNoteRepository.GetListAsync(x => x.IsPublished);
return items
.OrderByDescending(note => note.ReleaseDate)
.ThenByDescending(note => note.Version)
.Select(note => new ReleaseNoteDto
{
Id = note.Id,
Version = note.Version,
ReleaseDate = note.ReleaseDate,
Commit = note.Commit,
ChangeLog = note.GetChangeLog()
})
.ToList();
}
}

View file

@ -97,5 +97,6 @@ public enum TableNameEnum
Sequence, Sequence,
DbTableColumn, DbTableColumn,
DevRequest, DevRequest,
Agenda Agenda,
ReleaseNote
} }

View file

@ -15,6 +15,7 @@ public static class TableNameResolver
{ nameof(TableNameEnum.DbTableColumn), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.DbTableColumn), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.AiBot), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.AiBot), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ReleaseNote), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.GlobalSearch), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.GlobalSearch), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ContactTitle), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.ContactTitle), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.Currency), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.Currency), (TablePrefix.PlatformByName, MenuPrefix.Saas) },

View file

@ -0,0 +1,61 @@
using System;
using System.Linq;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
/// <summary>
/// Uygulamanın sürüm notları. Tek sürüm kaynağıdır: istemci sürüm numarasını ve
/// changelog'u application-configuration yanıtındaki
/// <c>extraProperties.changeLogs</c> alanından okur, ayrı bir sürüm dosyası yoktur.
///
/// Kayıtlar git tag'lerinden türer ve açılışta <c>ReleaseNoteStartupService</c>
/// tarafından tag listesiyle birebir eşitlenir.
///
/// Host seviyesindedir (tenant'a göre değişmez).
/// </summary>
public class ReleaseNote : FullAuditedEntity<Guid>
{
/// <summary>Kullanıcıya gösterilen sürüm numarası (ör. "1.2.1").</summary>
public string Version { get; set; }
/// <summary>Sürümün yayın tarihi; changelog bu alana göre sıralanır.</summary>
public DateTime ReleaseDate { get; set; }
/// <summary>İlgili commit (opsiyonel, izlenebilirlik için).</summary>
public string Commit { get; set; }
/// <summary>Satır başına bir madde olacak şekilde sürüm notları.</summary>
public string Notes { get; set; }
/// <summary>Yayından kaldırılan sürümler listede gösterilmez.</summary>
public bool IsPublished { get; set; }
/// <summary>
/// Notları maddelere böler. Tek yerde durmalı: hem changelog endpoint'i hem
/// application-configuration aynı biçimi döner.
/// </summary>
public string[] GetChangeLog()
{
return string.IsNullOrWhiteSpace(Notes)
? []
: Notes
.Split('\n')
.Select(line => line.Trim().TrimStart('-', '*', ' '))
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToArray();
}
protected ReleaseNote()
{
}
public ReleaseNote(string version, DateTime releaseDate, string notes, string commit = null, bool isPublished = true)
{
Version = version;
ReleaseDate = releaseDate;
Notes = notes;
Commit = commit;
IsPublished = isPublished;
}
}

View file

@ -46,6 +46,7 @@ public class PlatformDbContext :
public DbSet<Tenant> Tenants { get; set; } public DbSet<Tenant> Tenants { get; set; }
public DbSet<TenantConnectionString> TenantConnectionStrings { get; set; } public DbSet<TenantConnectionString> TenantConnectionStrings { get; set; }
public DbSet<AiBot> AiBots { get; set; } public DbSet<AiBot> AiBots { get; set; }
public DbSet<ReleaseNote> ReleaseNotes { get; set; }
public DbSet<Branch> Branches { get; set; } public DbSet<Branch> Branches { get; set; }
public DbSet<BranchUsers> BranchUsers { get; set; } public DbSet<BranchUsers> BranchUsers { get; set; }
public DbSet<GlobalSearch> GlobalSearchs { get; set; } public DbSet<GlobalSearch> GlobalSearchs { get; set; }
@ -202,6 +203,18 @@ public class PlatformDbContext :
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter(null); b.HasIndex(x => new { x.Name }).IsUnique().HasFilter(null);
}); });
builder.Entity<ReleaseNote>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReleaseNote)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(a => a.Version).IsRequired().HasMaxLength(32);
b.Property(a => a.Commit).HasMaxLength(64);
b.Property(a => a.Notes).HasColumnType("nvarchar(max)");
b.HasIndex(x => new { x.Version }).IsUnique().HasFilter("[IsDeleted] = 0");
});
builder.Entity<Branch>(b => builder.Entity<Branch>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Branch)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Branch)), Prefix.DbSchema);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260811131124_Initial")] [Migration("20260812194642_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -4662,6 +4662,68 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_Product", (string)null); b.ToTable("Sas_H_Product", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ReleaseNote", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Commit")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("datetime2");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Sas_H_ReleaseNote", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b => modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -1700,6 +1700,29 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_H_Product", x => x.Id); table.PrimaryKey("PK_Sas_H_Product", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_H_ReleaseNote",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Version = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
Commit = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsPublished = table.Column<bool>(type: "bit", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_ReleaseNote", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_H_Route", name: "Sas_H_Route",
columns: table => new columns: table => new
@ -4234,6 +4257,13 @@ namespace Sozsoft.Platform.Migrations
unique: true, unique: true,
filter: "[IsDeleted] = 0"); filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_ReleaseNote_Version",
table: "Sas_H_ReleaseNote",
column: "Version",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sas_H_Route_Key", name: "IX_Sas_H_Route_Key",
table: "Sas_H_Route", table: "Sas_H_Route",
@ -4574,6 +4604,9 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_Product"); name: "Sas_H_Product");
migrationBuilder.DropTable(
name: "Sas_H_ReleaseNote");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_Route"); name: "Sas_H_Route");

View file

@ -4659,6 +4659,68 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_Product", (string)null); b.ToTable("Sas_H_Product", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ReleaseNote", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Commit")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("datetime2");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Sas_H_ReleaseNote", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b => modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -23,7 +23,26 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
context.ApplicationConfiguration.SetProperty("environment", hostEnvironment.EnvironmentName); context.ApplicationConfiguration.SetProperty("environment", hostEnvironment.EnvironmentName);
var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>(); var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
context.ApplicationConfiguration.SetProperty("version", configuration.GetValue<string>("App:Version")); context.ApplicationConfiguration.SetProperty("apiVersion", configuration.GetValue<string>("App:Version"));
// Uygulamanın çalışan sürümü: istemcinin tek sürüm kaynağı (ayrı bir
// version.json isteği yoktur). Yalnızca EN SON sürüm taşınır; tam liste
// ChangeLog ekranının kendi endpoint'inden (/api/app/release-note) gelir,
// aksi halde her config isteğinde bütün changelog gönderilirdi.
var releaseNoteRepository = context.ServiceProvider.GetRequiredService<IRepository<ReleaseNote, Guid>>();
var releaseNotes = await releaseNoteRepository.GetListAsync(x => x.IsPublished);
var latest = releaseNotes
.OrderByDescending(note => note.ReleaseDate)
.ThenByDescending(note => note.Version)
.FirstOrDefault();
context.ApplicationConfiguration.SetProperty("changeLogs", new
{
uiVersion = latest?.Version,
buildDate = latest?.ReleaseDate.ToString("yyyy-MM-dd"),
tags = latest?.GetChangeLog() ?? []
});
// GetListAsync boş liste döner, null dönmez — bu yüzden null kontrolü yok. // GetListAsync boş liste döner, null dönmez — bu yüzden null kontrolü yok.
// Veritabanı erişilemezse çağrı exception atar; bu durum istek boru hattında, // Veritabanı erişilemezse çağrı exception atar; bu durum istek boru hattında,
@ -49,5 +68,6 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
context.ApplicationConfiguration.SetProperty("currentUserSessionId", sessionId); context.ApplicationConfiguration.SetProperty("currentUserSessionId", sessionId);
} }
} }
} }

View file

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Sozsoft.Platform.Extensions;
public class ReleaseNoteSeedDto
{
public string Version { get; set; }
public string ReleaseDate { get; set; }
public string Commit { get; set; }
public List<string> Notes { get; set; }
}
public class ReleaseNoteSeedFileDto
{
public List<ReleaseNoteSeedDto> ReleaseNotes { get; set; }
}
/// <summary>
/// Sürüm notlarını uygulama açılışında veritabanına senkronize eder.
///
/// Kaynak, imaja gömülen <c>release-notes.json</c> dosyasıdır; bu dosya build
/// sırasında <c>api/scripts/generate-release-notes.sh</c> tarafından git
/// tag'lerinden üretilir. Böylece yeni bir sürüm tag'i eklemek için DbMigrator
/// çalıştırmak GEREKMEZ — API'yi deploy etmek yeterlidir.
///
/// Tablo git tag listesinin birebir aynadır: yeni tag eklenir, mesajı düzeltilen
/// tag güncellenir, silinen tag veritabanından da kaldırılır. Dosya okunamaz veya
/// boşsa hiçbir şey yapılmaz (yanlışlıkla tablo boşaltılmasın diye).
/// </summary>
public class ReleaseNoteStartupService : IHostedService
{
private const string FileName = "release-notes.json";
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly IHostEnvironment _environment;
private readonly ILogger<ReleaseNoteStartupService> _logger;
public ReleaseNoteStartupService(
IServiceProvider serviceProvider,
IHostApplicationLifetime applicationLifetime,
IHostEnvironment environment,
ILogger<ReleaseNoteStartupService> logger)
{
_serviceProvider = serviceProvider;
_applicationLifetime = applicationLifetime;
_environment = environment;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_applicationLifetime.ApplicationStarted.Register(() =>
{
_ = Task.Run(SyncAsync);
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private async Task SyncAsync()
{
var path = Path.Combine(_environment.ContentRootPath, FileName);
if (!File.Exists(path))
{
_logger.LogWarning("{FileName} bulunamadı, sürüm notları senkronize edilmedi.", FileName);
return;
}
// Veritabanı henüz hazır olmayabilir (kurulum modu, migration sürüyor).
const int maxAttempts = 5;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
await using var stream = File.OpenRead(path);
var data = await JsonSerializer.DeserializeAsync<ReleaseNoteSeedFileDto>(
stream,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var items = data?.ReleaseNotes ?? [];
if (items.Count == 0)
{
return;
}
using var scope = _serviceProvider.CreateScope();
var (inserted, updated, deleted) = await SyncItemsAsync(scope.ServiceProvider, items);
_logger.LogInformation(
"Sürüm notları senkronize edildi: {Total} tag, {Inserted} eklendi, {Updated} güncellendi, {Deleted} silindi.",
items.Count,
inserted,
updated,
deleted);
return;
}
catch (Exception ex)
{
if (attempt == maxAttempts)
{
_logger.LogError(ex, "Sürüm notları senkronize edilemedi.");
return;
}
_logger.LogWarning(ex, "Sürüm notları senkronizasyonu başarısız, yeniden denenecek.");
await Task.Delay(TimeSpan.FromSeconds(3));
}
}
}
private static async Task<(int Inserted, int Updated, int Deleted)> SyncItemsAsync(
IServiceProvider services,
List<ReleaseNoteSeedDto> items)
{
var repository = services.GetRequiredService<IRepository<ReleaseNote, Guid>>();
var unitOfWorkManager = services.GetRequiredService<IUnitOfWorkManager>();
var currentTenant = services.GetRequiredService<ICurrentTenant>();
// Tablo host seviyesindedir; istek bağlamı olmadığı için tenant açıkça boş.
using (currentTenant.Change(null))
using (var uow = unitOfWorkManager.Begin(requiresNew: true))
{
var existingList = await repository.GetListAsync();
var existing = existingList.ToDictionary(note => note.Version, StringComparer.OrdinalIgnoreCase);
var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var inserted = 0;
var updated = 0;
foreach (var item in items.Where(item => !string.IsNullOrWhiteSpace(item.Version)))
{
tags.Add(item.Version);
var releaseDate = ParseDate(item.ReleaseDate);
var notes = string.Join(Environment.NewLine, item.Notes ?? []);
if (!existing.TryGetValue(item.Version, out var current))
{
await repository.InsertAsync(new ReleaseNote(item.Version, releaseDate, notes, item.Commit));
inserted++;
continue;
}
// Tag mesajı sonradan düzeltilmiş olabilir; yalnızca gerçekten
// değişen kayıt yazılır.
if (current.Notes == notes && current.ReleaseDate == releaseDate && current.Commit == item.Commit)
{
continue;
}
current.Notes = notes;
current.ReleaseDate = releaseDate;
current.Commit = item.Commit;
await repository.UpdateAsync(current);
updated++;
}
// Git'ten silinen tag'ler tabloda kalmamalı. Soft delete değil hard
// delete: aynı sürüm numarası tekrar tag'lenirse Version üzerindeki
// benzersiz indeks silinmiş satıra takılmasın.
var orphans = existingList.Where(note => !tags.Contains(note.Version)).ToList();
if (orphans.Count > 0)
{
await repository.HardDeleteAsync(orphans);
}
await uow.CompleteAsync();
return (inserted, updated, orphans.Count);
}
}
private static DateTime ParseDate(string value)
{
return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
? parsed
: DateTime.UtcNow;
}
}

View file

@ -140,6 +140,9 @@ public class PlatformHttpApiHostModule : AbpModule
ConfigureCache(); ConfigureCache();
ConfigureHangfire(context, configuration); ConfigureHangfire(context, configuration);
context.Services.AddHostedService<BackgroundWorkerStartupService>(); context.Services.AddHostedService<BackgroundWorkerStartupService>();
// Sürüm notları imaja gömülü release-notes.json'dan senkronize edilir;
// yeni bir sürüm tag'i için DbMigrator çalıştırmak gerekmez.
context.Services.AddHostedService<Extensions.ReleaseNoteStartupService>();
ConfigureBlobStoring(configuration); ConfigureBlobStoring(configuration);
ConfigureAuditing(); ConfigureAuditing();

View file

@ -45,5 +45,16 @@
<None Remove="Logs\**" /> <None Remove="Logs\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Sürüm notları: build sırasında git tag'lerinden üretilir, açılışta
ReleaseNoteStartupService tarafından veritabanına senkronize edilir. -->
<!-- Web SDK json dosyalarını zaten Content olarak alır; burada yalnızca
kopyalama davranışı güncelleniyor (Include duplicate hatası verir). -->
<Content Update="release-notes.json">
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,230 @@
{
"ReleaseNotes": [
{
"version": "1.2.0",
"releaseDate": "2026-08-13",
"commit": "798acbef9e6a26e29e58f10b13cb8e63ac5fa6b4",
"notes": [
"Claude.ai optimizasyon güncellemesi",
"Migrations ve Seeds klasör düzenlemesi",
"New Version Check komponent düzenlemesi"
]
},
{
"version": "1.1.10",
"releaseDate": "2026-07-31",
"commit": "2eef3ead35848341350659828a14dc4a463e57af",
"notes": [
"Kullanıcılara User Type sütunu eklendi. Hem \"İç Kullanıcılar\" hemde \"Dış Kullanıcılar\" tanımlanabiliyor.",
"Lisans adedi sadece İç Kullanıcılar üzerinden çalışıyor.",
"Scheduler ve TodoBard komponentler geliştirildi.",
"Lookup editorlerin Static değerlere dil desteği eklendi."
]
},
{
"version": "1.1.09",
"releaseDate": "2026-07-29",
"commit": "b0e78ac619dfebac2eb17edfc04551217ca4b93e",
"notes": [
"Wizard yetenekleri artırıldı. Menu Order eklendi.",
"GanttView sample düzenlemeleri",
"Schedulerview sample düzenlemeleri",
"CryptographyXml version 10.0.10"
]
},
{
"version": "1.1.08",
"releaseDate": "2026-07-16",
"commit": "24e92fcb82eef0ee26ca5d65e08c5404f132a09e",
"notes": [
"CardView komponenti geliştirildi.",
"Todo / Kanban komponenti geliştirildi.",
"Licence information",
"Check New Versiyon komponenti."
]
},
{
"version": "1.1.07",
"releaseDate": "2026-07-13",
"commit": "50cb3eaf979eb5027172a92a4e941ed64aa3671e",
"notes": [
"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",
"releaseDate": "2026-07-06",
"commit": "bab639ce6343b451b81d6688e4f7af666f0233da",
"notes": [
"Çekirdek kod için optimizasyon",
"List Reportlara Gruplu ve Alttoplam eklendi.",
"Paketlerdeki deprecated uyarları kaldırıldı"
]
},
{
"version": "1.1.05",
"releaseDate": "2026-06-27",
"commit": "352cf047b0af1b8aad53b632a41161efcc6cecdf",
"notes": [
"Ai için Ollama kurulumu yapıldı. Ayrıca Gemma LLM ile çalıştırıldı.",
"DynamicReport eklendi. ReportDesigner ile çalışan bir rapor tasarlandı.",
"DynamicGridReport eklendi. Grid üzerinde otomatik rapor alınabiliyor.",
"DynamicTreeReport eklendi. Tree üzerinden otomatik rapor alınabiliyor.",
"DynamicFormReport eklendi. Single ekranlarda otomatik rapor eklendi.",
"Demo sayfası menüden ayrıldı."
]
},
{
"version": "1.1.04",
"releaseDate": "2026-06-04",
"commit": "20e7fae481ce69e9a678508ce03b5ed7831aea9f",
"notes": [
"Settingde yapılan ayarlar Auth komponentlerine uygulandı.",
"Public home ve diğer sayfaların tasarım değişikliği yapıldı.",
"Route Type Dinamik ve Normal olarak ayrıldı.",
"Form Devexpress DefaultValue özelliği eklendi.",
"Devexpress DarkModa uygun şekilde güncellendi.",
"Grid, Tree ve FormDevexpress setReadonly özelliği eklendi."
]
},
{
"version": "1.1.03",
"releaseDate": "2026-05-30",
"commit": "96f7091d46c248ba3c42849fe5d870db0ab96982",
"notes": [
"User Detail komponentinin içerisinde Avatar ekleme",
"EditForm un içerisinde EditorOptions dinamik oluşturulması",
"Editform un içerisinde EditorScript dinamik oluşturulması"
]
},
{
"version": "1.1.02",
"releaseDate": "2026-05-27",
"commit": "84b9f6510787bd82f3797728fd675f755b4caa2d",
"notes": [
".NET 10 yükseltildi.",
"Abp Framework 10 yükseltildi.",
"Sql Query Manager problemleri giderildi.",
"AuditLog problemleri giderildi.",
"Tenan yapısını uygunluğu için düzenlemeler yapıldı."
]
},
{
"version": "1.1.01",
"releaseDate": "2026-05-24",
"commit": "6262baa6f12d695a25d83304af985092715d439a",
"notes": [
"Workflow tanımlaması yapılabilir.",
"Dark mod için uygulama güncellemesi",
"Form Field kısmında düzenleme",
"Hangfire Recurring Job düzenlemesi."
]
},
{
"version": "1.0.10",
"releaseDate": "2026-05-11",
"commit": "414006204e324018be597a598d3dd102868c5cca",
"notes": [
"Backup dosyalarını son 5 günün kalması",
"Grid için Fit Columns özelliği eklendi.",
"Notification Desktop, UiActivity, UiToast özelliği eklendi",
"Versiyon güncellemeleri için \"System Updating\" mesajı"
]
},
{
"version": "1.0.9",
"releaseDate": "2026-05-09",
"commit": "e9d8f5ebac35fc7b21f08139dfe175ed548c14b4",
"notes": [
"Video Rooms düzenlemesi",
"Genel Liste düzenlemeleri"
]
},
{
"version": "1.0.8",
"releaseDate": "2026-05-07",
"commit": "6fa266f23ed4f67384b7bef0e4030420cb788af5",
"notes": [
"Intranet ve Sosyal Duvar",
"Listelerde ImageUploadandViewer",
"Survey Widget",
"Event, Category ve Type"
]
},
{
"version": "1.0.7",
"releaseDate": "2026-05-04",
"commit": "4444fce93b4f62ad6698099fefe7da1614d2859c",
"notes": [
"Departman tanımlama",
"Job Position tanımlama",
"Wizard ve SQl Data güncellemeleri",
"Organizasyon Şemanı eklendi"
]
},
{
"version": "1.0.6",
"releaseDate": "2026-05-03",
"commit": "b2dfb04879a93709b04f8f0fc3bd4f5af1f7c01b",
"notes": [
"SalesOrder ve SalesOrderItem listFormu tanımlandı.",
"Sql Backup dosyalarını File Manager içerisinde görüntülenebiliyor.",
"Wizard File Manager komponenti oluşturuldu.",
"Wizard komponentini Edit özelliği eklendi."
]
},
{
"version": "1.0.5",
"releaseDate": "2026-04-28",
"commit": "b9cc68ff41d675fefadab79f5ea2c5e7f376708a",
"notes": [
"AllowAdding özelliği eklendi.",
"Kulllanıcı bazında Workhour özelliği eklendi.",
"ConcurrentUser sınırlaması getirildi.",
"Uygulamadan kullanıcıyı kickleme özelliği getirildi.",
"Uygulama açıkken DbMigrator çağrısı yapıldı."
]
},
{
"version": "1.0.4",
"releaseDate": "2026-03-30",
"commit": "e9ce256c0706de408e08a462bee9e4a72653f2c7",
"notes": [
"Role Yetkilerini kopyalama",
"Place Holder güncellemesi",
"Kayıt kopyalama özelliği"
]
},
{
"version": "1.0.3",
"releaseDate": "2026-03-28",
"commit": "130d35c37703935e34948419f2ee900cc1b6b28c",
"notes": [
"Report Template View And Design"
]
},
{
"version": "1.0.2",
"releaseDate": "2026-03-21",
"commit": "196b0dc24b7a815586166679b58ab4f9334bd40e",
"notes": [
"Artificial Intelligence Chat Bot güncellemeleri"
]
},
{
"version": "1.0.1",
"releaseDate": "2026-03-18",
"commit": "ac7784c030825158641e9042c50dede2067c0486",
"notes": [
"Public Site Designer",
"Menu Groups",
"Sql Query Manager",
"Listform Wizard"
]
}
]
}

View file

@ -2,7 +2,6 @@
set -e set -e
cd ~/sozsoft-platform cd ~/sozsoft-platform
git restore ui/public/version.json 2>/dev/null || git checkout -- ui/public/version.json
./configs/deployment/scripts/build/api.sh ./configs/deployment/scripts/build/api.sh
./configs/deployment/scripts/build/migrator.sh ./configs/deployment/scripts/build/migrator.sh

View file

@ -1,8 +1,14 @@
cd ~/sozsoft-platform cd ~/sozsoft-platform
git checkout main git checkout main
git fetch && git pull # --tags --force: sürüm notları git tag'lerinden üretiliyor; yerel tag origin'den
# farklı commit'e bakıyorsa --force olmadan reddedilir ve yeni tag'ler gelmez.
git fetch --prune --prune-tags --tags --force && git pull
cd ~/sozsoft-platform/api cd ~/sozsoft-platform/api
# Sürüm notları git tag'lerinden üretilir ve API imajına gömülür; uygulama
# açılışta bunları veritabanına senkronize eder (DbMigrator gerekmez).
./scripts/generate-release-notes.sh
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

@ -1,6 +1,8 @@
cd ~/sozsoft-platform cd ~/sozsoft-platform
git checkout main git checkout main
git fetch && git pull # --tags --force: sürüm notları git tag'lerinden üretiliyor; yerel tag origin'den
# farklı commit'e bakıyorsa --force olmadan reddedilir ve yeni tag'ler gelmez.
git fetch --prune --prune-tags --tags --force && git pull
cd ~/sozsoft-platform/api cd ~/sozsoft-platform/api

View file

@ -7,19 +7,20 @@ git checkout main
# --prune-tags: sunucuda elle atılmış, origin'de olmayan yerel tag'leri siler. # --prune-tags: sunucuda elle atılmış, origin'de olmayan yerel tag'leri siler.
# Bunlar temizlenmezse `git tag` listesinde kalır ve deploy yanlış (daha yüksek) # Bunlar temizlenmezse `git tag` listesinde kalır ve deploy yanlış (daha yüksek)
# bir sürüm numarasıyla etiketlenir. # bir sürüm numarasıyla etiketlenir.
git fetch --prune --prune-tags --tags && git pull git fetch --prune --prune-tags --tags --force && git pull
cd ~/sozsoft-platform/ui cd ~/sozsoft-platform/ui
# Sürüm, HEAD'den erişilebilen son tag'dir (başka daldaki tag'ler sayılmaz); # Yalnızca imaj etiketi: HEAD'den erişilebilen son tag (başka daldaki tag'ler
# tag yoksa package.json'daki sürüm kullanılır. # sayılmaz), tag yoksa kısa commit. Uygulamanın gösterdiği sürüm buradan değil,
# backend'deki sürüm notları tablosundan gelir.
VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//') VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
VERSION=${VERSION:-$(jq -r .version package.json)} VERSION=${VERSION:-$(git rev-parse --short HEAD)}
echo "Build versiyonu: ${VERSION}" echo "Build versiyonu: ${VERSION}"
# Deploy öncesi git üzerindeki tüm versiyon bilgilerini güncelle # Sürüm numarası ve sürüm notları backend'de (Sürüm Notları tablosu) tutulur;
./scripts/generate-version.sh # UI tarafında üretilecek bir sürüm dosyası yoktur. Buradaki VERSION yalnızca
# imaj etiketidir.
docker build \ docker build \
-t devops.sozsoft.com/sozsoft/sozsoft-platform-ui:${ENV}-${VERSION} \ -t devops.sozsoft.com/sozsoft/sozsoft-platform-ui:${ENV}-${VERSION} \
-t devops.sozsoft.com/sozsoft/sozsoft-platform-ui:${ENV}-latest \ -t devops.sozsoft.com/sozsoft/sozsoft-platform-ui:${ENV}-latest \

View file

@ -1,4 +1,3 @@
VITE_API_URL='https://localhost:44344' VITE_API_URL='https://localhost:44344'
VITE_CDN_URL='http://localhost:4005' VITE_CDN_URL='http://localhost:4005'
VITE_REACT_APP_VERSION=$npm_package_version
VITE_AI_URL='https://ai.sozsoft.com/webhook/' VITE_AI_URL='https://ai.sozsoft.com/webhook/'

View file

@ -1,13 +1,12 @@
{ {
"name": "sozsoft-platform-ui", "name": "sozsoft-platform-ui",
"private": true, "private": true,
"version": "1.0.7",
"elstarVersion": "2.1.6", "elstarVersion": "2.1.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "node scripts/generate-version.js && vite build", "build": "vite build",
"build:production": "node scripts/generate-version.js && vite build", "build:production": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",

View file

@ -1,234 +0,0 @@
{
"buildId": "98e9f84.msq55max",
"buildTime": "2026-08-12T13:44:50.404Z",
"buildDate": "2026-08-12",
"commit": "98e9f84",
"packageVersion": "1.0.7",
"version": "1.2.1",
"releases": [
{
"version": "1.2.1",
"buildDate": "2026-08-11",
"commit": "783c1fe1d35f7cb8f23fb63fad26123cbe54aafc",
"changeLog": [
"Claude.ai güncellemesi"
]
},
{
"version": "1.1.10",
"buildDate": "2026-07-31",
"commit": "2eef3ead35848341350659828a14dc4a463e57af",
"changeLog": [
"Kullanıcılara User Type sütunu eklendi. Hem \"İç Kullanıcılar\" hemde \"Dış Kullanıcılar\" tanımlanabiliyor.",
"Lisans adedi sadece İç Kullanıcılar üzerinden çalışıyor.",
"Scheduler ve TodoBard komponentler geliştirildi.",
"Lookup editorlerin Static değerlere dil desteği eklendi."
]
},
{
"version": "1.1.09",
"buildDate": "2026-07-29",
"commit": "b0e78ac619dfebac2eb17edfc04551217ca4b93e",
"changeLog": [
"Wizard yetenekleri artırıldı. Menu Order eklendi.",
"GanttView sample düzenlemeleri",
"Schedulerview sample düzenlemeleri",
"CryptographyXml version 10.0.10"
]
},
{
"version": "1.1.08",
"buildDate": "2026-07-16",
"commit": "24e92fcb82eef0ee26ca5d65e08c5404f132a09e",
"changeLog": [
"CardView komponenti geliştirildi.",
"Todo / Kanban komponenti geliştirildi.",
"Licence information",
"Check New Versiyon komponenti."
]
},
{
"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",
"commit": "bab639ce6343b451b81d6688e4f7af666f0233da",
"changeLog": [
"Çekirdek kod için optimizasyon",
"List Reportlara Gruplu ve Alttoplam eklendi.",
"Paketlerdeki deprecated uyarları kaldırıldı"
]
},
{
"version": "1.1.05",
"buildDate": "2026-06-27",
"commit": "352cf047b0af1b8aad53b632a41161efcc6cecdf",
"changeLog": [
"Ai için Ollama kurulumu yapıldı. Ayrıca Gemma LLM ile çalıştırıldı.",
"DynamicReport eklendi. ReportDesigner ile çalışan bir rapor tasarlandı.",
"DynamicGridReport eklendi. Grid üzerinde otomatik rapor alınabiliyor.",
"DynamicTreeReport eklendi. Tree üzerinden otomatik rapor alınabiliyor.",
"DynamicFormReport eklendi. Single ekranlarda otomatik rapor eklendi.",
"Demo sayfası menüden ayrıldı."
]
},
{
"version": "1.1.04",
"buildDate": "2026-06-04",
"commit": "20e7fae481ce69e9a678508ce03b5ed7831aea9f",
"changeLog": [
"Settingde yapılan ayarlar Auth komponentlerine uygulandı.",
"Public home ve diğer sayfaların tasarım değişikliği yapıldı.",
"Route Type Dinamik ve Normal olarak ayrıldı.",
"Form Devexpress DefaultValue özelliği eklendi.",
"Devexpress DarkModa uygun şekilde güncellendi.",
"Grid, Tree ve FormDevexpress setReadonly özelliği eklendi."
]
},
{
"version": "1.1.03",
"buildDate": "2026-05-30",
"commit": "96f7091d46c248ba3c42849fe5d870db0ab96982",
"changeLog": [
"User Detail komponentinin içerisinde Avatar ekleme",
"EditForm un içerisinde EditorOptions dinamik oluşturulması",
"Editform un içerisinde EditorScript dinamik oluşturulması"
]
},
{
"version": "1.1.02",
"buildDate": "2026-05-27",
"commit": "84b9f6510787bd82f3797728fd675f755b4caa2d",
"changeLog": [
".NET 10 yükseltildi.",
"Abp Framework 10 yükseltildi.",
"Sql Query Manager problemleri giderildi.",
"AuditLog problemleri giderildi.",
"Tenan yapısını uygunluğu için düzenlemeler yapıldı."
]
},
{
"version": "1.1.01",
"buildDate": "2026-05-24",
"commit": "6262baa6f12d695a25d83304af985092715d439a",
"changeLog": [
"Workflow tanımlaması yapılabilir.",
"Dark mod için uygulama güncellemesi",
"Form Field kısmında düzenleme",
"Hangfire Recurring Job düzenlemesi."
]
},
{
"version": "1.0.10",
"buildDate": "2026-05-11",
"commit": "414006204e324018be597a598d3dd102868c5cca",
"changeLog": [
"Backup dosyalarını son 5 günün kalması",
"Grid için Fit Columns özelliği eklendi.",
"Notification Desktop, UiActivity, UiToast özelliği eklendi",
"Versiyon güncellemeleri için \"System Updating\" mesajı"
]
},
{
"version": "1.0.9",
"buildDate": "2026-05-09",
"commit": "e9d8f5ebac35fc7b21f08139dfe175ed548c14b4",
"changeLog": [
"Video Rooms düzenlemesi",
"Genel Liste düzenlemeleri"
]
},
{
"version": "1.0.8",
"buildDate": "2026-05-07",
"commit": "6fa266f23ed4f67384b7bef0e4030420cb788af5",
"changeLog": [
"Intranet ve Sosyal Duvar",
"Listelerde ImageUploadandViewer",
"Survey Widget",
"Event, Category ve Type"
]
},
{
"version": "1.0.7",
"buildDate": "2026-05-04",
"commit": "4444fce93b4f62ad6698099fefe7da1614d2859c",
"changeLog": [
"Departman tanımlama",
"Job Position tanımlama",
"Wizard ve SQl Data güncellemeleri",
"Organizasyon Şemanı eklendi"
]
},
{
"version": "1.0.6",
"buildDate": "2026-05-03",
"commit": "b2dfb04879a93709b04f8f0fc3bd4f5af1f7c01b",
"changeLog": [
"SalesOrder ve SalesOrderItem listFormu tanımlandı.",
"Sql Backup dosyalarını File Manager içerisinde görüntülenebiliyor.",
"Wizard File Manager komponenti oluşturuldu.",
"Wizard komponentini Edit özelliği eklendi."
]
},
{
"version": "1.0.5",
"buildDate": "2026-04-28",
"commit": "b9cc68ff41d675fefadab79f5ea2c5e7f376708a",
"changeLog": [
"AllowAdding özelliği eklendi.",
"Kulllanıcı bazında Workhour özelliği eklendi.",
"ConcurrentUser sınırlaması getirildi.",
"Uygulamadan kullanıcıyı kickleme özelliği getirildi.",
"Uygulama açıkken DbMigrator çağrısı yapıldı."
]
},
{
"version": "1.0.4",
"buildDate": "2026-03-30",
"commit": "e9ce256c0706de408e08a462bee9e4a72653f2c7",
"changeLog": [
"Role Yetkilerini kopyalama",
"Place Holder güncellemesi",
"Kayıt kopyalama özelliği"
]
},
{
"version": "1.0.3",
"buildDate": "2026-03-28",
"commit": "130d35c37703935e34948419f2ee900cc1b6b28c",
"changeLog": [
"Report Template View And Design"
]
},
{
"version": "1.0.2",
"buildDate": "2026-03-21",
"commit": "196b0dc24b7a815586166679b58ab4f9334bd40e",
"changeLog": [
"Artificial Intelligence Chat Bot güncellemeleri"
]
},
{
"version": "1.0.1",
"buildDate": "2026-03-18",
"commit": "ac7784c030825158641e9042c50dede2067c0486",
"changeLog": [
"Public Site Designer",
"Menu Groups",
"Sql Query Manager",
"Listform Wizard"
]
}
]
}

View file

@ -1,117 +0,0 @@
// scripts/generate-version.js
//
// public/version.json üretir. Bu dosya uygulamanın tek sürüm kaynağıdır:
// - `buildId` : HER deploy'da değişir (commit + build zamanı). Uygulama açık
// sekmede ve açılışta yeni deploy'u bununla anlar; git tag
// atılmamış olsa bile güncelleme algılanır.
// - `version` : Kullanıcıya gösterilen sürüm (en güncel release).
// - `releases` : Changelog kaynağı. Git tag'lerinden okunur; HEAD tag'li
// değilse son tag'den bu yana atılan commit'ler "yayınlanmamış"
// sürüm olarak en üste eklenir, böylece her deploy'un notu olur.
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
function safeExec(cmd) {
try {
return execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'], shell: true }).toString().trim()
} catch {
return null
}
}
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const uiRoot = path.resolve(scriptDir, '..')
const outputFile = path.join(uiRoot, 'public', 'version.json')
const pkg = JSON.parse(fs.readFileSync(path.join(uiRoot, 'package.json'), 'utf8'))
const packageVersion = pkg.version || '0.0.0'
const buildTime = new Date().toISOString()
const buildDate = buildTime.slice(0, 10)
const hasGit = !!safeExec('git rev-parse --is-inside-work-tree')
/**
* Docker imajı içinde `.git` yoktur; sürüm bilgisi host'ta (deploy script'i ile)
* üretilip `public/version.json` olarak kopyalanır. Bu durumda dosyayı yeniden
* üretmek gerçek changelog'u ve `buildId`'yi silip yerine "yerel derleme"
* yazardı; bu yüzden mevcut dosya korunur.
*/
if (!hasGit && fs.existsSync(outputFile)) {
const existing = JSON.parse(fs.readFileSync(outputFile, 'utf8'))
const version = existing.version ?? existing.releases?.[0]?.version ?? packageVersion
const buildId = existing.buildId ?? existing.commit ?? `${packageVersion}.${Date.now().toString(36)}`
if (!existing.buildId || !existing.version) {
existing.buildId = buildId
existing.version = version
fs.writeFileSync(outputFile, JSON.stringify(existing, null, 2))
}
console.log(`> mevcut version.json korundu: v${version} (${buildId})`)
process.exit(0)
}
const shortCommit = process.env.GIT_COMMIT || (hasGit ? safeExec('git rev-parse --short HEAD') : null)
const commit = shortCommit || 'local'
/** Tag mesajını changelog satırlarına çevirir. */
const toChangeLog = (raw) =>
(raw ?? '')
.split('\n')
.map((line) => line.replace(/^\s*[-*]\s*/, '').trim())
.filter(Boolean)
let releases = []
if (hasGit) {
// Sıralama tag adına göre (creatordate değil): sunucuda sonradan atılmış bir
// tag listeyi başa geçirip yanlış sürüm göstermesin.
const rawTags = safeExec('git tag --list --sort=-v:refname')
const tags = rawTags ? rawTags.split('\n').filter(Boolean) : []
releases = tags
.map((tag) => {
const version = tag.replace(/^v/, '')
const date = safeExec(`git log -1 --format=%ad --date=short ${tag}`)
const commitId = safeExec(`git rev-list -n 1 ${tag}`)
const changeLog = toChangeLog(safeExec(`git tag -l --format="%(contents)" ${tag}`))
return {
version,
buildDate: date || buildDate,
commit: commitId,
changeLog: changeLog.length ? changeLog : ['Bu sürüm için not girilmemiş'],
}
})
// Changelog yalnızca tag'lerden oluşur. Tag'siz commit'ler için sahte bir
// sürüm üretilmez; sürüm notlarını tag mesajı belirler. Deploy algılaması
// sürüm numarasına değil, her derlemede değişen `buildId`'ye bakar.
}
if (releases.length === 0) {
releases = [
{
version: packageVersion,
buildDate,
commit,
changeLog: ['Yerel geliştirme derlemesi', 'Git bilgisi bulunamadı, package.json sürümü kullanıldı'],
},
]
}
const versionInfo = {
// Her derlemede değişir; uygulamanın deploy algılaması buna bakar.
buildId: `${commit}.${Date.now().toString(36)}`,
buildTime,
buildDate,
commit,
packageVersion,
version: releases[0].version,
releases,
}
fs.mkdirSync(path.dirname(outputFile), { recursive: true })
fs.writeFileSync(outputFile, JSON.stringify(versionInfo, null, 2))
console.log(`> version.json: v${versionInfo.version} (${versionInfo.buildId})`)

View file

@ -1,50 +0,0 @@
#!/bin/bash
set -e
# public/version.json üretir (deploy öncesi, HOST üzerinde).
#
# Asıl uygulama scripts/generate-version.js dosyasıdır; node varsa o çalışır.
# Aşağıdaki bash karşılığı yalnızca node bulunmayan sunucular içindir ve
# js sürümüyle aynı çıktıyı üretmelidir.
cd "$(dirname "$0")/.."
echo "> version.json oluşturuluyor..."
if command -v node >/dev/null 2>&1; then
node scripts/generate-version.js
exit 0
fi
# origin'de olmayan yerel tag'ler sürüm numarasını bozar; temizle.
git fetch --prune --prune-tags --tags origin >/dev/null 2>&1 || true
# Sürüm sırası tag adına göre (creatordate değil); 1.1.10 > 1.1.09.
VERSIONS=$(git tag --sort=-v:refname)
ROOT_COMMIT=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
BUILD_DATE=$(date -u +%Y-%m-%d)
# Her deploy'da değişir; uygulamanın yeni sürüm algılaması buna bakar.
BUILD_ID="${ROOT_COMMIT}.$(date -u +%s)"
RELEASES=""
FIRST=true
for TAG in $VERSIONS; do
VER=$(echo "$TAG" | sed 's/^v//')
DATE=$(git log -1 --format=%ad --date=short "$TAG")
COMMIT=$(git rev-list -n 1 "$TAG")
MESSAGE=$(git tag -l --format="%(contents)" "$TAG" | grep -v '^$' | sed 's/^[[:space:]]*-[[:space:]]*//' | jq -R . | jq -s .)
if [ "$FIRST" = true ]; then FIRST=false; else RELEASES+=","; fi
RELEASES+="{\"version\":\"$VER\",\"buildDate\":\"$DATE\",\"commit\":\"$COMMIT\",\"changeLog\":$MESSAGE}"
done
# Sürüm = en son tag. Tag'siz commit'ler için sahte sürüm üretilmez; deploy
# algılaması sürüm numarasına değil, her derlemede değişen buildId'ye bakar.
LAST_TAG=$(echo "$VERSIONS" | head -n1 | sed 's/^v//')
VERSION=${LAST_TAG:-$(jq -r .version package.json)}
printf '{"buildId":"%s","buildTime":"%s","buildDate":"%s","commit":"%s","version":"%s","releases":[%s]}' \
"$BUILD_ID" "$BUILD_TIME" "$BUILD_DATE" "$ROOT_COMMIT" "$VERSION" "$RELEASES" | jq . > public/version.json
echo "> version.json: v${VERSION} (${BUILD_ID})"

View file

@ -34,7 +34,7 @@ const FooterContent = () => {
{apiConfig && ( {apiConfig && (
<span> <span>
<b>API: </b> <b>API: </b>
{apiConfig["environment"].toString()}:{apiConfig["version"].toString()} {apiConfig["environment"].toString()}:{apiConfig["apiVersion"].toString()}
</span> </span>
)} )}
</div> </div>

View file

@ -10,7 +10,7 @@ import { Helmet } from 'react-helmet'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { getSetupStatus } from '@/services/setup.service' import { getSetupStatus } from '@/services/setup.service'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { getRunningVersion } from '@/views/version/swRegistration' import type { VersionInfo } from '@/views/version/swRegistration'
let didInit = false let didInit = false
@ -74,12 +74,15 @@ const Theme = (props: CommonProps) => {
useDarkMode() useDarkMode()
useTabFocus() useTabFocus()
// Çalışan bundle'ın sürümü footer'da gösterilir; derleme anında gömülüdür, // Sürüm numarası backend'den (application-configuration) gelir; footer bunu
// ağ isteği gerektirmez. // gösterir.
const setUiVersion = useStoreActions((actions) => actions.locale.setUiVersion) const setUiVersion = useStoreActions((actions) => actions.locale.setUiVersion)
const appVersion = useStoreState(
(state) => state.abpConfig.config?.extraProperties?.['changeLogs'],
) as VersionInfo | undefined
useEffect(() => { useEffect(() => {
setUiVersion(getRunningVersion()) setUiVersion(appVersion?.uiVersion)
}, [setUiVersion]) }, [appVersion, setUiVersion])
type ConfigProviderValue = NonNullable<ComponentProps<typeof ConfigProvider>['value']> type ConfigProviderValue = NonNullable<ComponentProps<typeof ConfigProvider>['value']>

View file

@ -0,0 +1,26 @@
import apiService from '@/services/api.service'
/** `Sas_H_ReleaseNote` kaydı; git tag'lerinden tohumlanır. */
export interface ReleaseNoteDto {
id: string
version: string
releaseDate: string
commit?: string
changeLog: string[]
}
class ReleaseNoteService {
/**
* ChangeLog ekranının kaynağı. Uygulamanın çalışan sürüm numarası
* application-configuration ile geldiği için burada yalnızca tam liste alınır.
*/
async getList(): Promise<ReleaseNoteDto[]> {
const response = await apiService.fetchData<ReleaseNoteDto[]>({
url: '/api/app/release-note',
method: 'GET',
})
return response.data
}
}
export const releaseNoteService = new ReleaseNoteService()

View file

@ -10,6 +10,7 @@ import {
ApplicationConfigurationDto, ApplicationConfigurationDto,
ApplicationLocalizationRequestDto, ApplicationLocalizationRequestDto,
} from '../proxy/config/models' } from '../proxy/config/models'
import { setAppVersionInfo } from '../views/version/swRegistration'
export type Texts = Record<string, Record<string, string>> export type Texts = Record<string, Record<string, string>>
@ -66,6 +67,10 @@ export const abpConfigModel: AbpConfigModel = {
actions.setConfig(result.data) actions.setConfig(result.data)
// Sürüm ve sürüm notları backend'den bu yanıtla gelir; güncelleme motoru
// ayrı bir istek atmaz.
setAppVersionInfo(result.data.extraProperties?.['changeLogs'])
//Eğer login değilse //Eğer login değilse
if (result.data.currentUser.isAuthenticated) { if (result.data.currentUser.isAuthenticated) {
await actions.getMenu() await actions.getMenu()

View file

@ -55,7 +55,6 @@ const ASSET_CACHE = 'sozsoft-assets-v1'
const THEME_CACHE = 'sozsoft-themes-v1' const THEME_CACHE = 'sozsoft-themes-v1'
const FONT_CACHE = 'sozsoft-fonts-v1' const FONT_CACHE = 'sozsoft-fonts-v1'
const IMAGE_CACHE = 'sozsoft-images-v1' const IMAGE_CACHE = 'sozsoft-images-v1'
const VERSION_CACHE = 'sozsoft-version-v1'
const KNOWN_CACHES = [ const KNOWN_CACHES = [
SHELL_CACHE, SHELL_CACHE,
@ -63,7 +62,6 @@ const KNOWN_CACHES = [
THEME_CACHE, THEME_CACHE,
FONT_CACHE, FONT_CACHE,
IMAGE_CACHE, IMAGE_CACHE,
VERSION_CACHE,
] ]
/** /**
@ -577,9 +575,6 @@ self.addEventListener('message', (event) => {
event.waitUntil(ensureWarmup()) event.waitUntil(ensureWarmup())
return return
} }
if (type === 'PING') {
event.source?.postMessage({ type: 'PONG', shellSize: MANIFEST.length })
}
}) })
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -623,21 +618,6 @@ async function staleWhileRevalidate(request: Request, cacheName: string, limit:
return cached ?? (await network) ?? Response.error() return cached ?? (await network) ?? Response.error()
} }
async function networkFirst(request: Request, cacheName: string, timeoutMs: number) {
const cache = await caches.open(cacheName)
try {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
const response = await fetch(request, { signal: controller.signal })
clearTimeout(timer)
if (response.ok) await cache.put(request, response.clone())
return response
} catch {
const cached = await cache.match(request)
return cached ?? Response.error()
}
}
/** Kabuktaki index.html'i arka planda tazeler (deploy sonrası kendini onarır). */ /** Kabuktaki index.html'i arka planda tazeler (deploy sonrası kendini onarır). */
async function refreshIndex(cache: Cache, key: string, url: string) { async function refreshIndex(cache: Cache, key: string, url: string) {
try { try {
@ -681,12 +661,6 @@ self.addEventListener('fetch', (event) => {
return return
} }
// Sürüm bilgisi asla eski kalmamalı.
if (url.pathname.endsWith('/version.json')) {
event.respondWith(networkFirst(request, VERSION_CACHE, 3000))
return
}
const shellKey = shellKeys.get(url.href.split('?')[0]) const shellKey = shellKeys.get(url.href.split('?')[0])
if (shellKey) { if (shellKey) {
event.respondWith( event.respondWith(

View file

@ -41,8 +41,6 @@ const titleFor = (state: AppUpdateState) => {
return 'Yeni sürüm hazırlanıyor' return 'Yeni sürüm hazırlanıyor'
case 'activating': case 'activating':
return 'Uygulama yeniden başlatılıyor' return 'Uygulama yeniden başlatılıyor'
case 'reload-required':
return 'Yeni sürüm yayınlandı'
case 'error': case 'error':
return 'Güncelleme tamamlanamadı' return 'Güncelleme tamamlanamadı'
default: default:
@ -65,8 +63,6 @@ const descriptionFor = (state: AppUpdateState) => {
return 'İndirme tamamlandı, yeni sürüme geçiliyor.' return 'İndirme tamamlandı, yeni sürüme geçiliyor.'
case 'activating': case 'activating':
return 'Sayfa otomatik olarak yenilenecek.' return 'Sayfa otomatik olarak yenilenecek.'
case 'reload-required':
return `Yeni sürüme otomatik geçiliyor${countdownText(state)}.`
case 'error': case 'error':
return `${state.message ?? 'Geçiş tamamlanamadı.'}${countdownText(state)}` return `${state.message ?? 'Geçiş tamamlanamadı.'}${countdownText(state)}`
default: default:
@ -133,7 +129,6 @@ const AppUpdateDialog = () => {
const busy = state.status === 'downloading' || state.status === 'ready' const busy = state.status === 'downloading' || state.status === 'ready'
const doneCount = state.files.filter((file) => file.status === 'done').length const doneCount = state.files.filter((file) => file.status === 'done').length
const upgrading = !!state.version && state.version !== state.currentVersion
const ordered = state.files.slice().sort(byActivity) const ordered = state.files.slice().sort(byActivity)
return ( return (
@ -159,17 +154,8 @@ const AppUpdateDialog = () => {
<div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs"> <div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs">
<span className="rounded-full bg-gray-100 px-2.5 py-1 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300"> <span className="rounded-full bg-gray-100 px-2.5 py-1 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{upgrading ? 'Mevcut' : 'Sürüm'}: v{state.currentVersion} Sürüm: v{state.currentVersion}
</span> </span>
{/* İlk kurulumda geçilecek bir sürüm yok; aynı numarayı iki kez göstermeyelim. */}
{upgrading && (
<>
<span className="text-gray-400"></span>
<span className="rounded-full bg-indigo-50 px-2.5 py-1 font-semibold text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-300">
Yeni: v{state.version}
</span>
</>
)}
</div> </div>
{(busy || state.files.length > 0) && ( {(busy || state.files.length > 0) && (
@ -213,7 +199,7 @@ const AppUpdateDialog = () => {
{state.changeLog.length > 0 && ( {state.changeLog.length > 0 && (
<div className="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700"> <div className="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700">
<p className="text-xs font-bold uppercase tracking-wide text-gray-400"> <p className="text-xs font-bold uppercase tracking-wide text-gray-400">
v{state.version} ile gelen yenilikler v{state.currentVersion} ile gelen yenilikler
</p> </p>
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300"> <ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300">
{state.changeLog.map((line, index) => ( {state.changeLog.map((line, index) => (
@ -242,14 +228,13 @@ const AppUpdateDialog = () => {
{state.status === 'completed' ? 'Tamam' : 'Arka planda devam et'} {state.status === 'completed' ? 'Tamam' : 'Arka planda devam et'}
</button> </button>
) : ( ) : (
(state.status === 'reload-required' || state.status === 'error') && ( state.status === 'error' && (
<button <button
type="button" type="button"
className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700" className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
onClick={applyAppUpdate} onClick={applyAppUpdate}
> >
{state.status === 'error' ? 'Şimdi tekrar dene' : 'Şimdi geç'} Şimdi tekrar dene{countdownText(state)}
{countdownText(state)}
</button> </button>
) )
)} )}

View file

@ -6,13 +6,8 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa' import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa'
import { checkForAppUpdate, clearPendingChangelog, fetchVersionInfo } from './swRegistration' import { releaseNoteService, type ReleaseNoteDto } from '@/services/releaseNote.service'
import { checkForAppUpdate, clearPendingChangelog } from './swRegistration'
type Release = {
version: string
buildDate: string
changeLog: string[]
}
// 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
@ -111,16 +106,21 @@ const Log = ({
const Changelog = () => { const Changelog = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
const [releases, setReleases] = useState<Release[]>([])
const { isCheckingUpdate, checkForUpdate } = useManualUpdateCheck() const { isCheckingUpdate, checkForUpdate } = useManualUpdateCheck()
// Kaynak `ReleaseNote` entity'sidir; liste yalnızca bu ekran açıldığında
// istenir (application-configuration sadece çalışan sürümü taşır).
const [releases, setReleases] = useState<ReleaseNoteDto[]>([])
useEffect(() => { useEffect(() => {
// Bekleyen changelog bayrağı burada tüketilir; kullanıcı değişiklikleri // Bekleyen changelog bayrağı burada tüketilir; kullanıcı değişiklikleri
// gördüğü için sonraki girişlerde tekrar yönlendirilmez. // gördüğü için sonraki girişlerde tekrar yönlendirilmez.
clearPendingChangelog() clearPendingChangelog()
// Açılışta zaten alınan sürüm bilgisi paylaşılır (ek istek atılmaz). void releaseNoteService
void fetchVersionInfo().then((info) => setReleases((info?.releases as Release[]) ?? [])) .getList()
.then(setReleases)
.catch((error: unknown) => console.warn('Sürüm notları alınamadı.', error))
}, []) }, [])
return ( return (
@ -150,8 +150,8 @@ const Changelog = () => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{releases.map((rel) => ( {releases.map((rel) => (
<Log key={rel.version} version={rel.version} date={rel.buildDate}> <Log key={rel.version} version={rel.version} date={rel.releaseDate.slice(0, 10)}>
{rel.changeLog?.length > 0 && ( {!!rel.changeLog?.length && (
<ul className="list-none mt-2 space-y-2"> <ul className="list-none mt-2 space-y-2">
{rel.changeLog.map((item, i) => ( {rel.changeLog.map((item, i) => (
<li key={i} className="flex items-start"> <li key={i} className="flex items-start">

View file

@ -5,23 +5,27 @@ import './installPrompt'
/** /**
* Sürüm / güncelleme motoru * Sürüm / güncelleme motoru
* ------------------------------------------------------------------ * ------------------------------------------------------------------
* Tek sürüm kaynağı `public/version.json` (build'de `scripts/generate-version.js` * Sürüm numarası ve sürüm notları backend'den gelir: application-configuration
* üretir). Çalışan bundle'ın `buildId`'si derleme anında gömülür; sunucudaki * yanıtındaki `extraProperties.changeLogs`. Store, config'i her aldığında
* `version.json` ile karşılaştırıldığı için git tag'i atılmasa bile YENİ DEPLOY * `setAppVersionInfo()` ile bu modülü besler; ayrı bir sürüm isteği yoktur.
* kesin olarak anlaşılır. *
* Deploy algılaması service worker'ın kendi güncelleme kontrolüne dayanır:
* herhangi bir chunk değiştiğinde entry chunk'ın hash'i, dolayısıyla index.html
* ve `sw.js` içindeki precache manifest'i değişir; `registration.update()` yeni
* worker'ı görür. Böylece ayrı bir sürüm dosyası yoklamaya gerek kalmaz.
* *
* Akış: * Akış:
* 1. ılışta tek bir `version.json` isteği (sonuç önbelleklenir, diğer * 1. Service worker kaydı + periyodik/olay tabanlı `update()` kontrolü.
* ekranlar `fetchVersionInfo()` ile aynı sonucu kullanır). * 2. Yeni worker bulunursa kurulum başlar; `src/sw.ts` indirdiği her dosyayı
* 2. `buildId` farklıysa service worker güncellemesi tetiklenir. * isim/boyut/yüzde olarak yayınlar, bu modül state'e çevirir.
* 3. Service worker (`src/sw.ts`) indirdiği her dosyayı isim/boyut/yüzde * 3. Kurulum bitince SKIP_WAITING `controllerchange` reload.
* olarak yayınlar; bu modül state'e çevirir, pencere (lazy) gösterir. * 4. Backend'den gelen sürüm son görülenden farklıysa changelog bayrağı
* 4. Kurulum bitince SKIP_WAITING `controllerchange` reload. * yazılır; login sonrası kullanıcı ChangeLog ekranına yönlendirilir.
* 5. Yeni build ilk kez çalıştığında changelog bayrağı yazılır; login sonrası * Tetikleyici sürümdür, bundle değil: yalnızca yeni bir sürüm notu (git
* kullanıcı ChangeLog ekranına yönlendirilir. * tag) eklendiğinde de kullanıcı değişiklikleri görür.
* *
* Bu dosya entry chunk'ındadır: React/UI bağımlılığı yoktur, pencere ve * Bu dosya entry chunk'ındadır: React/UI/store bağımlılığı yoktur, pencere
* bildirim bileşenleri yalnızca gerçekten güncelleme varken lazy yüklenir. * yalnızca gerçekten indirme varken lazy yüklenir.
*/ */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -58,16 +62,13 @@ export type AppUpdateStatus =
/** Tüm bileşenler indi ve geçiş gerekmiyor (ilk kurulum). */ /** Tüm bileşenler indi ve geçiş gerekmiyor (ilk kurulum). */
| 'completed' | 'completed'
| 'activating' | 'activating'
| 'reload-required'
| 'error' | 'error'
export type AppUpdateState = { export type AppUpdateState = {
status: AppUpdateStatus status: AppUpdateStatus
/** Pencere açık mı. */ /** Pencere açık mı. */
visible: boolean visible: boolean
/** Sunucudaki (yeni) sürüm. */ /** Uygulamanın çalışan sürümü (backend'den gelir). */
version?: string
/** Çalışan bundle'ın sürümü. */
currentVersion: string currentVersion: string
changeLog: string[] changeLog: string[]
files: UpdateFile[] files: UpdateFile[]
@ -89,50 +90,34 @@ export type AppUpdateState = {
dismissible: boolean dismissible: boolean
} }
export type Release = { /**
version: string * `extraProperties.changeLogs` yükü: yalnızca ÇALIŞAN sürüm. Tam sürüm notu
buildDate?: string * listesi ChangeLog ekranının kendi endpoint'inden gelir, her config isteğinde
commit?: string * taşınmaz.
changeLog?: string[] */
}
export type VersionInfo = { export type VersionInfo = {
buildId: string /** Uygulamanın (UI) çalışan sürümü. */
version: string uiVersion: string
buildTime?: string
buildDate?: string buildDate?: string
commit?: string /** Son sürümün notları. */
releases: Release[] tags?: string[]
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Sabitler */ /* Sabitler */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
declare const __APP_BUILD_ID__: string
declare const __APP_VERSION__: string
const RUNNING_BUILD_ID = typeof __APP_BUILD_ID__ === 'string' ? __APP_BUILD_ID__ : 'dev'
const RUNNING_VERSION = typeof __APP_VERSION__ === 'string' ? __APP_VERSION__ : '0.0.0'
/** Sekme açık kalırsa periyodik yoklama. */ /** Sekme açık kalırsa periyodik yoklama. */
const VERSION_POLL_INTERVAL = 30 * 60 * 1000 const VERSION_POLL_INTERVAL = 30 * 60 * 1000
/** Aynı version.json sonucunun paylaşıldığı süre (açılışta tek istek). */
const VERSION_CACHE_TTL = 60 * 1000
/** İki deploy yoklaması arasındaki en kısa süre (sekmeye dönüş, online vb.). */
const MIN_VERSION_CHECK_GAP = 5 * 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
const REGISTRATION_TIMEOUT = 10_000 const REGISTRATION_TIMEOUT = 10_000
const UPDATE_CALL_TIMEOUT = 20_000 const UPDATE_CALL_TIMEOUT = 20_000
/** Yenileme gerektiren deploy'da otomatik geçişe kalan süre. */
const AUTO_RELOAD_SECONDS = 5
/** 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_BUILD_KEY = 'sozsoft:last-build-id' const LAST_VERSION_KEY = 'sozsoft:last-version'
const AUTO_RELOAD_GUARD_KEY = 'sozsoft:auto-reload-build'
const PENDING_CHANGELOG_KEY = 'sozsoft:pending-changelog' const PENDING_CHANGELOG_KEY = 'sozsoft:pending-changelog'
const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload' const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload'
@ -170,7 +155,8 @@ const writeStorage = (key: string, value: string | undefined, session = false) =
let state: AppUpdateState = { let state: AppUpdateState = {
status: 'idle', status: 'idle',
visible: false, visible: false,
currentVersion: RUNNING_VERSION, // Backend'den (`setAppVersionInfo`) gelene kadar boş kalır.
currentVersion: '',
changeLog: [], changeLog: [],
files: [], files: [],
loadedBytes: 0, loadedBytes: 0,
@ -254,90 +240,43 @@ function recalculate(
/* Sürüm bilgisi */ /* Sürüm bilgisi */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
let versionInfoPromise: Promise<VersionInfo | undefined> | undefined
let inFlightVersionRequest: Promise<VersionInfo | undefined> | undefined
let lastVersionFetchAt = 0
let latestInfo: VersionInfo | undefined
const normalize = (raw: unknown): VersionInfo | undefined => {
const data = raw as Partial<VersionInfo> | undefined
if (!data) return undefined
const releases = Array.isArray(data.releases) ? data.releases : []
const version = data.version ?? releases[0]?.version
if (!version) return undefined
return {
buildId: String(data.buildId ?? data.commit ?? version),
version,
buildTime: data.buildTime,
buildDate: data.buildDate ?? releases[0]?.buildDate,
commit: data.commit,
releases,
}
}
/** /**
* `version.json` tek sürüm kaynağı (deploy yoklaması + changelog). * Sürüm bilgisini application-configuration yanıtından alır.
* *
* Uçuşta bir istek varsa ikinci bir isteği atılmaz; `force` verilmediğinde * Store, config'i her yüklediğinde bunu çırır; ayrı bir sürüm isteği yoktur.
* kısa süre içinde (TTL) alınan sonuç paylaşılır. Böylece ılışta motor, * Yük şekli: `extraProperties.changeLogs = { uiVersion, buildDate, tags[] }`.
* pencere ve ChangeLog ekranı tek isteği kullanır.
*/ */
export const fetchVersionInfo = (force = false): Promise<VersionInfo | undefined> => { export const setAppVersionInfo = (raw: unknown) => {
if (inFlightVersionRequest) return inFlightVersionRequest const data = raw as Partial<VersionInfo> | undefined
if (!force && versionInfoPromise && Date.now() - lastVersionFetchAt < VERSION_CACHE_TTL) { if (!data?.uiVersion) return
return versionInfoPromise
}
inFlightVersionRequest = fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store' }) setState({
.then((response) => { currentVersion: data.uiVersion,
if (!response.ok) throw new Error(`version.json isteği ${response.status} döndü`) changeLog: Array.isArray(data.tags) ? data.tags.filter(Boolean) : [],
return response.json()
})
.then((data) => {
const info = normalize(data)
if (info) {
latestInfo = info
lastVersionFetchAt = Date.now()
}
return info
})
.catch((error: unknown) => {
console.warn('Sürüm bilgisi okunamadı.', error)
versionInfoPromise = undefined
return undefined
})
.finally(() => {
inFlightVersionRequest = undefined
}) })
versionInfoPromise = inFlightVersionRequest markVersionSeen(data.uiVersion)
return inFlightVersionRequest
} }
export const getRunningVersion = () => RUNNING_VERSION
export const getLatestVersionInfo = () => latestInfo
/** Login sonrası changelog yönlendirmesi için bayrak. */ /** Login sonrası changelog yönlendirmesi için bayrak. */
export const hasPendingChangelog = () => !!readStorage(PENDING_CHANGELOG_KEY) export const hasPendingChangelog = () => !!readStorage(PENDING_CHANGELOG_KEY)
export const consumePendingChangelog = (): string | undefined => {
const value = readStorage(PENDING_CHANGELOG_KEY)
if (value) writeStorage(PENDING_CHANGELOG_KEY, undefined)
return value
}
export const clearPendingChangelog = () => writeStorage(PENDING_CHANGELOG_KEY, undefined) export const clearPendingChangelog = () => writeStorage(PENDING_CHANGELOG_KEY, undefined)
/** /**
* Yeni bir build ilk kez çalıştığında (güncelleme sonrası ya da kullanıcı * Sürüm numarası son görülenden farklıysa changelog bayrağını yazar; kullanıcı
* uygulamaya deploy'dan sonra ilk kez girdiğinde) changelog bayrağını yazar. * 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 markRunningBuildSeen(version: string) { function markVersionSeen(version: string) {
const lastSeen = readStorage(LAST_BUILD_KEY) const lastSeen = readStorage(LAST_VERSION_KEY)
if (lastSeen && lastSeen !== RUNNING_BUILD_ID) { if (lastSeen === version) return
writeStorage(PENDING_CHANGELOG_KEY, version) if (lastSeen) writeStorage(PENDING_CHANGELOG_KEY, version)
} writeStorage(LAST_VERSION_KEY, version)
if (lastSeen !== RUNNING_BUILD_ID) writeStorage(LAST_BUILD_KEY, RUNNING_BUILD_ID)
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -518,7 +457,7 @@ function activateWaitingWorker() {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
type SwFile = { type SwFile = {
index?: number index: number
url: string url: string
name: string name: string
loaded: number loaded: number
@ -528,7 +467,7 @@ type SwFile = {
type SwProgressMessage = { type SwProgressMessage = {
type?: string type?: string
/** Tam liste (START / DONE). */ /** Tam liste (START / STATE / DONE). */
files?: SwFile[] files?: SwFile[]
/** Yalnızca değişen dosyalar (PROGRESS). */ /** Yalnızca değişen dosyalar (PROGRESS). */
updates?: SwFile[] updates?: SwFile[]
@ -540,8 +479,8 @@ type SwProgressMessage = {
} }
const toFiles = (files: SwFile[] | undefined): UpdateFile[] => const toFiles = (files: SwFile[] | undefined): UpdateFile[] =>
(files ?? []).map((file, position) => ({ (files ?? []).map((file) => ({
index: file.index ?? position, index: file.index,
url: file.url, url: file.url,
name: file.name, name: file.name,
loaded: file.loaded ?? 0, loaded: file.loaded ?? 0,
@ -625,8 +564,7 @@ function handleWorkerMessage(event: MessageEvent) {
case 'SW_PRECACHE_PROGRESS': case 'SW_PRECACHE_PROGRESS':
// Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır. // Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır.
if (!dialogMountRequested) void showUpdateDialog('downloading') if (!dialogMountRequested) void showUpdateDialog('downloading')
// `files`: yalnızca eski service worker'lar (tam liste) için. recalculate(mergeFiles(toFiles(data.updates)), totals, {
recalculate(mergeFiles(toFiles(data.updates ?? data.files)), totals, {
status: 'downloading', status: 'downloading',
visible: true, visible: true,
dismissible: !transitions, dismissible: !transitions,
@ -696,64 +634,12 @@ async function showUpdateDialog(status: AppUpdateStatus) {
/* Sürüm kontrolü */ /* Sürüm kontrolü */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function applyVersionInfo(info: VersionInfo) {
const release = info.releases[0]
setState({
version: info.version,
changeLog: release?.changeLog?.filter(Boolean) ?? [],
})
}
/** /**
* Sunucudaki build çalışan build'den farklı mı? * Deploy'da service worker dosyası mutlaka değişir: herhangi bir chunk'ın
* * hash'i değiştiğinde entry chunk, dolayısıyla index.html ve `sw.js` içindeki
* Dev'de karşılaştırma yapılmaz: `__APP_BUILD_ID__` vite config yüklenirken * precache manifest'i de değişir. Bu yüzden ayrı bir sürüm dosyası yoklamaya
* gömüldüğü için `version.json` yeniden üretildiğinde sunucu yeniden * gerek yok; `registration.update()` yeni deploy'u tek başına yakalar.
* başlatılana kadar kalıcı olarak farklı görünür ve sonsuz yenileme oluşur.
*/ */
const isNewDeploy = (info: VersionInfo | undefined) =>
!!info && import.meta.env.PROD && RUNNING_BUILD_ID !== 'dev' && info.buildId !== RUNNING_BUILD_ID
/** Deploy yoklaması: `version.json` okunur (sürüm + changelog aynı dosyada). */
async function checkVersionEndpoint(force = false) {
const now = Date.now()
if (now - lastVersionCheckAt < (force ? MIN_CHECK_GAP : MIN_VERSION_CHECK_GAP)) return false
lastVersionCheckAt = now
const info = await fetchVersionInfo(force)
if (!info) return false
applyVersionInfo(info)
if (!isNewDeploy(info)) return false
const requireReload = () => {
void showUpdateDialog('reload-required')
// Yenileme sonrası hâlâ aynı build çalışıyorsa (ör. sunucu eski dosyayı
// servis etmeye devam ediyorsa) otomatik yenilemeyi tekrarlamak sonsuz
// döngü demektir; bu durumda karar kullanıcıya bırakılır.
if (readStorage(AUTO_RELOAD_GUARD_KEY, true) === info.buildId) {
setState({
secondsLeft: undefined,
message: 'Yeni sürüme geçilemedi. Yenilemeyi tekrar deneyebilirsiniz.',
})
return
}
writeStorage(AUTO_RELOAD_GUARD_KEY, info.buildId, true)
// Kullanıcı onayı beklenmez; kısa bir bilgilendirmeden sonra otomatik geçilir.
scheduleAutoAction(AUTO_RELOAD_SECONDS, () => void reloadWithFreshShell())
}
if (isServiceWorkerEnabled && 'serviceWorker' in navigator) {
await runUpdateCheck(true)
// Service worker aynı kabuk dosyalarını görüyorsa (ör. yalnızca lazy chunk
// değişmiş) kurulum tetiklenmez; bu durumda sayfa yenilemesi yeterlidir.
if (state.status === 'idle' || state.status === 'checking') requireReload()
} else {
requireReload()
}
return true
}
async function runUpdateCheck(force = false) { async function runUpdateCheck(force = false) {
if (!registration || activating || reloading) return if (!registration || activating || reloading) return
@ -871,17 +757,17 @@ function startWatchers() {
// yalnızca sekme uzun süre açık ve önde kalırsa devreye girer. // yalnızca sekme uzun süre açık ve önde kalırsa devreye girer.
window.clearInterval(pollTimer) window.clearInterval(pollTimer)
pollTimer = window.setInterval(() => { pollTimer = window.setInterval(() => {
if (document.visibilityState === 'visible') void checkVersionEndpoint() if (document.visibilityState === 'visible') void runUpdateCheck()
}, VERSION_POLL_INTERVAL) }, VERSION_POLL_INTERVAL)
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return if (document.visibilityState !== 'visible') return
void checkVersionEndpoint() void runUpdateCheck()
}) })
window.addEventListener('online', () => { window.addEventListener('online', () => {
// Çevrimdışıyken beklemeye alınan güncelleme bağlantı gelince sürer. // Çevrimdışıyken beklemeye alınan güncelleme bağlantı gelince sürer.
if (state.status === 'error') void retryUpdate() if (state.status === 'error') void retryUpdate()
else void checkVersionEndpoint() else void runUpdateCheck()
}) })
} }
@ -926,20 +812,11 @@ export const registerServiceWorker = () => {
started = true started = true
scope.__sozsoftUpdateEngineStarted = true scope.__sozsoftUpdateEngineStarted = true
// Sürüm okuma ve deploy kontrolü service worker'dan bağımsızdır; dev'de de // Açılış performansı: yoklama dinleyicileri ilk boyamayı beklesin.
// (SW kapalıyken) yeni deploy algılanır.
markRunningBuildSeen(RUNNING_VERSION)
// Açılış performansı: sürüm isteği ve SW kaydı ilk boyamayı beklesin.
// Açılışta `force` verilmez; bildirim kancasıyla aynı istek paylaşılır.
const boot = () => {
void checkVersionEndpoint()
startWatchers()
}
const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, o?: object) => void }) const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, o?: object) => void })
.requestIdleCallback .requestIdleCallback
if (idle) idle.call(window, boot, { timeout: 3_000 }) if (idle) idle.call(window, startWatchers, { timeout: 3_000 })
else window.setTimeout(boot, 1_500) else window.setTimeout(startWatchers, 1_500)
if (!('serviceWorker' in navigator)) return if (!('serviceWorker' in navigator)) return
@ -996,9 +873,6 @@ export const registerServiceWorker = () => {
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => { export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
setState({ status: state.status === 'idle' ? 'checking' : state.status }) setState({ status: state.status === 'idle' ? 'checking' : state.status })
const newDeploy = await checkVersionEndpoint(true)
if (newDeploy) return 'updating'
if (!('serviceWorker' in navigator)) return 'unsupported' if (!('serviceWorker' in navigator)) return 'unsupported'
if (!isServiceWorkerEnabled) return 'disabled' if (!isServiceWorkerEnabled) return 'disabled'

View file

@ -4,7 +4,6 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_URL: string readonly VITE_API_URL: string
readonly VITE_CDN_URL: string readonly VITE_CDN_URL: string
readonly VITE_REACT_APP_VERSION: string
readonly VITE_AI_URL: string readonly VITE_AI_URL: string
readonly VITE_USE_POLLING?: string readonly VITE_USE_POLLING?: string
/** Dev sunucusunda service worker'ı açar (varsayılan kapalı). */ /** Dev sunucusunda service worker'ı açar (varsayılan kapalı). */

View file

@ -1,7 +1,6 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import fs from 'fs'
import path from 'path' import path from 'path'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
@ -82,27 +81,8 @@ function warmupManifest(): Plugin {
} }
} }
/**
* `scripts/generate-version.js` build'den önce çalışır. Buradaki değerleri
* bundle'a gömüyoruz ki uygulama, çalışan kodun build kimliğini sunucudaki
* `version.json` ile birebir karşılaştırıp yeni deploy'u kesin olarak anlasın.
*/
function readBuildInfo() {
try {
const file = path.join(import.meta.dirname, 'public', 'version.json')
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
return {
buildId: String(data.buildId ?? data.commit ?? 'dev'),
version: String(data.version ?? data.releases?.[0]?.version ?? '0.0.0'),
}
} catch {
return { buildId: 'dev', version: '0.0.0' }
}
}
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '') const env = loadEnv(mode, process.cwd(), '')
const buildInfo = readBuildInfo()
const usePolling = env.VITE_USE_POLLING === 'true' const usePolling = env.VITE_USE_POLLING === 'true'
// Dev'de service worker varsayılan olarak kapalıdır; açık olduğunda HMR // Dev'de service worker varsayılan olarak kapalıdır; açık olduğunda HMR
// sonrası eski chunk'lar cache'den servis edilip kafa karıştırıyor. // sonrası eski chunk'lar cache'den servis edilip kafa karıştırıyor.
@ -292,10 +272,6 @@ export default defineConfig(({ mode }) => {
define: { define: {
'process.env': {}, 'process.env': {},
// Çalışan bundle'ın kimliği. İstemci bunu sunucudaki version.json ile
// karşılaştırarak yeni deploy'u kesin olarak (tag'e ihtiyaç duymadan) anlar.
__APP_BUILD_ID__: JSON.stringify(buildInfo.buildId),
__APP_VERSION__: JSON.stringify(buildInfo.version),
}, },
} }
}) })