ReleaseNoteAppService güncellemesi
This commit is contained in:
parent
534dcce6da
commit
0abf181a9c
35 changed files with 1012 additions and 704 deletions
24
README.md
24
README.md
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
86
api/scripts/generate-release-notes.sh
Normal file
86
api/scripts/generate-release-notes.sh
Normal 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"
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,5 +97,6 @@ public enum TableNameEnum
|
||||||
Sequence,
|
Sequence,
|
||||||
DbTableColumn,
|
DbTableColumn,
|
||||||
DevRequest,
|
DevRequest,
|
||||||
Agenda
|
Agenda,
|
||||||
|
ReleaseNote
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
|
|
||||||
61
api/src/Sozsoft.Platform.Domain/Entities/Host/ReleaseNote.cs
Normal file
61
api/src/Sozsoft.Platform.Domain/Entities/Host/ReleaseNote.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
230
api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json
Normal file
230
api/src/Sozsoft.Platform.HttpApi.Host/release-notes.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)"' \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
1
ui/.env
1
ui/.env
|
|
@ -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/'
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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})`)
|
|
||||||
|
|
@ -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})"
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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']>
|
||||||
|
|
||||||
|
|
|
||||||
26
ui/src/services/releaseNote.service.ts
Normal file
26
ui/src/services/releaseNote.service.ts
Normal 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()
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
26
ui/src/sw.ts
26
ui/src/sw.ts
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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. Açı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 ağ isteği atılmaz; `force` verilmediğinde
|
* Store, config'i her yüklediğinde bunu çağı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 açı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 açı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'
|
||||||
|
|
||||||
|
|
|
||||||
1
ui/src/vite-env.d.ts
vendored
1
ui/src/vite-env.d.ts
vendored
|
|
@ -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ı). */
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue