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.
|
||||
- `registerType: 'prompt'` — yeni sürüm indirilir, indirme bitince otomatik devreye alınır
|
||||
(`views/version/swRegistration.ts`, `sw.ts`, `AppUpdateDialog.tsx`).
|
||||
- `version.json` her zaman ağdan tazelenir (NetworkFirst); changelog `/admin/changeLog`
|
||||
ekranında gösterilir, yeni deploy sonrası ilk girişte oraya yönlendirilir.
|
||||
- `scripts/generate-version.js` build sırasında sürüm bilgisini üretir.
|
||||
- Isıtma yalnızca kuruluma bağlı değildir: uygulama açılışta `ENSURE_WARMUP` mesajı yollar,
|
||||
service worker eksik kalan bileşenleri kaldığı yerden tamamlar.
|
||||
- **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,
|
||||
DbTableColumn,
|
||||
DevRequest,
|
||||
Agenda
|
||||
Agenda,
|
||||
ReleaseNote
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public static class TableNameResolver
|
|||
{ nameof(TableNameEnum.DbTableColumn), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
|
||||
{ nameof(TableNameEnum.LogEntry), (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.ContactTitle), (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<TenantConnectionString> TenantConnectionStrings { get; set; }
|
||||
public DbSet<AiBot> AiBots { get; set; }
|
||||
public DbSet<ReleaseNote> ReleaseNotes { get; set; }
|
||||
public DbSet<Branch> Branches { get; set; }
|
||||
public DbSet<BranchUsers> BranchUsers { get; set; }
|
||||
public DbSet<GlobalSearch> GlobalSearchs { get; set; }
|
||||
|
|
@ -202,6 +203,18 @@ public class PlatformDbContext :
|
|||
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 =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Branch)), Prefix.DbSchema);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260811131124_Initial")]
|
||||
[Migration("20260812194642_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -4662,6 +4662,68 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1700,6 +1700,29 @@ namespace Sozsoft.Platform.Migrations
|
|||
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(
|
||||
name: "Sas_H_Route",
|
||||
columns: table => new
|
||||
|
|
@ -4234,6 +4257,13 @@ namespace Sozsoft.Platform.Migrations
|
|||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_ReleaseNote_Version",
|
||||
table: "Sas_H_ReleaseNote",
|
||||
column: "Version",
|
||||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_Route_Key",
|
||||
table: "Sas_H_Route",
|
||||
|
|
@ -4574,6 +4604,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Product");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_ReleaseNote");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Route");
|
||||
|
||||
|
|
@ -4659,6 +4659,68 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
|
|||
|
|
@ -23,7 +23,26 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
|
|||
context.ApplicationConfiguration.SetProperty("environment", hostEnvironment.EnvironmentName);
|
||||
|
||||
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.
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
ConfigureHangfire(context, configuration);
|
||||
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);
|
||||
ConfigureAuditing();
|
||||
|
||||
|
|
|
|||
|
|
@ -45,5 +45,16 @@
|
|||
<None Remove="Logs\**" />
|
||||
</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>
|
||||
|
||||
|
|
|
|||
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
|
||||
|
||||
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/migrator.sh
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
cd ~/sozsoft-platform
|
||||
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
|
||||
|
||||
# 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 \
|
||||
| grep -o '"[^"]*"\s*:\s*"[^"]*"' \
|
||||
| grep -E '^"(Version)"' \
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
cd ~/sozsoft-platform
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -7,19 +7,20 @@ git checkout main
|
|||
# --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)
|
||||
# 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
|
||||
|
||||
# Sürüm, HEAD'den erişilebilen son tag'dir (başka daldaki tag'ler sayılmaz);
|
||||
# tag yoksa package.json'daki sürüm kullanılır.
|
||||
# Yalnızca imaj etiketi: HEAD'den erişilebilen son tag (başka daldaki tag'ler
|
||||
# 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=${VERSION:-$(jq -r .version package.json)}
|
||||
VERSION=${VERSION:-$(git rev-parse --short HEAD)}
|
||||
echo "Build versiyonu: ${VERSION}"
|
||||
|
||||
# Deploy öncesi git üzerindeki tüm versiyon bilgilerini güncelle
|
||||
./scripts/generate-version.sh
|
||||
|
||||
# Sürüm numarası ve sürüm notları backend'de (Sürüm Notları tablosu) tutulur;
|
||||
# UI tarafında üretilecek bir sürüm dosyası yoktur. Buradaki VERSION yalnızca
|
||||
# imaj etiketidir.
|
||||
docker build \
|
||||
-t devops.sozsoft.com/sozsoft/sozsoft-platform-ui:${ENV}-${VERSION} \
|
||||
-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_CDN_URL='http://localhost:4005'
|
||||
VITE_REACT_APP_VERSION=$npm_package_version
|
||||
VITE_AI_URL='https://ai.sozsoft.com/webhook/'
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
{
|
||||
"name": "sozsoft-platform-ui",
|
||||
"private": true,
|
||||
"version": "1.0.7",
|
||||
"elstarVersion": "2.1.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "node scripts/generate-version.js && vite build",
|
||||
"build:production": "node scripts/generate-version.js && vite build",
|
||||
"build": "vite build",
|
||||
"build:production": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"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 && (
|
||||
<span>
|
||||
<b>API: </b>
|
||||
{apiConfig["environment"].toString()}:{apiConfig["version"].toString()}
|
||||
{apiConfig["environment"].toString()}:{apiConfig["apiVersion"].toString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Helmet } from 'react-helmet'
|
|||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { getSetupStatus } from '@/services/setup.service'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { getRunningVersion } from '@/views/version/swRegistration'
|
||||
import type { VersionInfo } from '@/views/version/swRegistration'
|
||||
|
||||
let didInit = false
|
||||
|
||||
|
|
@ -74,12 +74,15 @@ const Theme = (props: CommonProps) => {
|
|||
useDarkMode()
|
||||
useTabFocus()
|
||||
|
||||
// Çalışan bundle'ın sürümü footer'da gösterilir; derleme anında gömülüdür,
|
||||
// ağ isteği gerektirmez.
|
||||
// Sürüm numarası backend'den (application-configuration) gelir; footer bunu
|
||||
// gösterir.
|
||||
const setUiVersion = useStoreActions((actions) => actions.locale.setUiVersion)
|
||||
const appVersion = useStoreState(
|
||||
(state) => state.abpConfig.config?.extraProperties?.['changeLogs'],
|
||||
) as VersionInfo | undefined
|
||||
useEffect(() => {
|
||||
setUiVersion(getRunningVersion())
|
||||
}, [setUiVersion])
|
||||
setUiVersion(appVersion?.uiVersion)
|
||||
}, [appVersion, setUiVersion])
|
||||
|
||||
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,
|
||||
ApplicationLocalizationRequestDto,
|
||||
} from '../proxy/config/models'
|
||||
import { setAppVersionInfo } from '../views/version/swRegistration'
|
||||
|
||||
export type Texts = Record<string, Record<string, string>>
|
||||
|
||||
|
|
@ -66,6 +67,10 @@ export const abpConfigModel: AbpConfigModel = {
|
|||
|
||||
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
|
||||
if (result.data.currentUser.isAuthenticated) {
|
||||
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 FONT_CACHE = 'sozsoft-fonts-v1'
|
||||
const IMAGE_CACHE = 'sozsoft-images-v1'
|
||||
const VERSION_CACHE = 'sozsoft-version-v1'
|
||||
|
||||
const KNOWN_CACHES = [
|
||||
SHELL_CACHE,
|
||||
|
|
@ -63,7 +62,6 @@ const KNOWN_CACHES = [
|
|||
THEME_CACHE,
|
||||
FONT_CACHE,
|
||||
IMAGE_CACHE,
|
||||
VERSION_CACHE,
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -577,9 +575,6 @@ self.addEventListener('message', (event) => {
|
|||
event.waitUntil(ensureWarmup())
|
||||
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()
|
||||
}
|
||||
|
||||
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). */
|
||||
async function refreshIndex(cache: Cache, key: string, url: string) {
|
||||
try {
|
||||
|
|
@ -681,12 +661,6 @@ self.addEventListener('fetch', (event) => {
|
|||
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])
|
||||
if (shellKey) {
|
||||
event.respondWith(
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ const titleFor = (state: AppUpdateState) => {
|
|||
return 'Yeni sürüm hazırlanıyor'
|
||||
case 'activating':
|
||||
return 'Uygulama yeniden başlatılıyor'
|
||||
case 'reload-required':
|
||||
return 'Yeni sürüm yayınlandı'
|
||||
case 'error':
|
||||
return 'Güncelleme tamamlanamadı'
|
||||
default:
|
||||
|
|
@ -65,8 +63,6 @@ const descriptionFor = (state: AppUpdateState) => {
|
|||
return 'İndirme tamamlandı, yeni sürüme geçiliyor.'
|
||||
case 'activating':
|
||||
return 'Sayfa otomatik olarak yenilenecek.'
|
||||
case 'reload-required':
|
||||
return `Yeni sürüme otomatik geçiliyor${countdownText(state)}.`
|
||||
case 'error':
|
||||
return `${state.message ?? 'Geçiş tamamlanamadı.'}${countdownText(state)}`
|
||||
default:
|
||||
|
|
@ -133,7 +129,6 @@ const AppUpdateDialog = () => {
|
|||
|
||||
const busy = state.status === 'downloading' || state.status === 'ready'
|
||||
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)
|
||||
|
||||
return (
|
||||
|
|
@ -159,17 +154,8 @@ const AppUpdateDialog = () => {
|
|||
|
||||
<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">
|
||||
{upgrading ? 'Mevcut' : 'Sürüm'}: v{state.currentVersion}
|
||||
Sürüm: v{state.currentVersion}
|
||||
</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>
|
||||
|
||||
{(busy || state.files.length > 0) && (
|
||||
|
|
@ -213,7 +199,7 @@ const AppUpdateDialog = () => {
|
|||
{state.changeLog.length > 0 && (
|
||||
<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">
|
||||
v{state.version} ile gelen yenilikler
|
||||
v{state.currentVersion} ile gelen yenilikler
|
||||
</p>
|
||||
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300">
|
||||
{state.changeLog.map((line, index) => (
|
||||
|
|
@ -242,14 +228,13 @@ const AppUpdateDialog = () => {
|
|||
{state.status === 'completed' ? 'Tamam' : 'Arka planda devam et'}
|
||||
</button>
|
||||
) : (
|
||||
(state.status === 'reload-required' || state.status === 'error') && (
|
||||
state.status === 'error' && (
|
||||
<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"
|
||||
onClick={applyAppUpdate}
|
||||
>
|
||||
{state.status === 'error' ? 'Şimdi tekrar dene' : 'Şimdi geç'}
|
||||
{countdownText(state)}
|
||||
Şimdi tekrar dene{countdownText(state)}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,8 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa'
|
||||
import { checkForAppUpdate, clearPendingChangelog, fetchVersionInfo } from './swRegistration'
|
||||
|
||||
type Release = {
|
||||
version: string
|
||||
buildDate: string
|
||||
changeLog: string[]
|
||||
}
|
||||
import { releaseNoteService, type ReleaseNoteDto } from '@/services/releaseNote.service'
|
||||
import { checkForAppUpdate, clearPendingChangelog } from './swRegistration'
|
||||
|
||||
// User dropdown reuses the exact update-check flow owned by this component.
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
|
|
@ -111,16 +106,21 @@ const Log = ({
|
|||
|
||||
const Changelog = () => {
|
||||
const { translate } = useLocalization()
|
||||
const [releases, setReleases] = useState<Release[]>([])
|
||||
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(() => {
|
||||
// Bekleyen changelog bayrağı burada tüketilir; kullanıcı değişiklikleri
|
||||
// gördüğü için sonraki girişlerde tekrar yönlendirilmez.
|
||||
clearPendingChangelog()
|
||||
|
||||
// Açılışta zaten alınan sürüm bilgisi paylaşılır (ek istek atılmaz).
|
||||
void fetchVersionInfo().then((info) => setReleases((info?.releases as Release[]) ?? []))
|
||||
void releaseNoteService
|
||||
.getList()
|
||||
.then(setReleases)
|
||||
.catch((error: unknown) => console.warn('Sürüm notları alınamadı.', error))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
@ -150,8 +150,8 @@ const Changelog = () => {
|
|||
</div>
|
||||
<div className="space-y-1">
|
||||
{releases.map((rel) => (
|
||||
<Log key={rel.version} version={rel.version} date={rel.buildDate}>
|
||||
{rel.changeLog?.length > 0 && (
|
||||
<Log key={rel.version} version={rel.version} date={rel.releaseDate.slice(0, 10)}>
|
||||
{!!rel.changeLog?.length && (
|
||||
<ul className="list-none mt-2 space-y-2">
|
||||
{rel.changeLog.map((item, i) => (
|
||||
<li key={i} className="flex items-start">
|
||||
|
|
|
|||
|
|
@ -5,23 +5,27 @@ import './installPrompt'
|
|||
/**
|
||||
* Sürüm / güncelleme motoru
|
||||
* ------------------------------------------------------------------
|
||||
* Tek sürüm kaynağı `public/version.json` (build'de `scripts/generate-version.js`
|
||||
* üretir). Çalışan bundle'ın `buildId`'si derleme anında gömülür; sunucudaki
|
||||
* `version.json` ile karşılaştırıldığı için git tag'i atılmasa bile YENİ DEPLOY
|
||||
* kesin olarak anlaşılır.
|
||||
* Sürüm numarası ve sürüm notları backend'den gelir: application-configuration
|
||||
* yanıtındaki `extraProperties.changeLogs`. Store, config'i her aldığında
|
||||
* `setAppVersionInfo()` ile bu modülü besler; ayrı bir sürüm isteği yoktur.
|
||||
*
|
||||
* 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ış:
|
||||
* 1. Açılışta tek bir `version.json` isteği (sonuç önbelleklenir, diğer
|
||||
* ekranlar `fetchVersionInfo()` ile aynı sonucu kullanır).
|
||||
* 2. `buildId` farklıysa service worker güncellemesi tetiklenir.
|
||||
* 3. Service worker (`src/sw.ts`) indirdiği her dosyayı isim/boyut/yüzde
|
||||
* olarak yayınlar; bu modül state'e çevirir, pencere (lazy) gösterir.
|
||||
* 4. Kurulum bitince SKIP_WAITING → `controllerchange` → reload.
|
||||
* 5. Yeni build ilk kez çalıştığında changelog bayrağı yazılır; login sonrası
|
||||
* kullanıcı ChangeLog ekranına yönlendirilir.
|
||||
* 1. Service worker kaydı + periyodik/olay tabanlı `update()` kontrolü.
|
||||
* 2. Yeni worker bulunursa kurulum başlar; `src/sw.ts` indirdiği her dosyayı
|
||||
* isim/boyut/yüzde olarak yayınlar, bu modül state'e çevirir.
|
||||
* 3. Kurulum bitince SKIP_WAITING → `controllerchange` → reload.
|
||||
* 4. Backend'den gelen sürüm son görülenden farklıysa changelog bayrağı
|
||||
* yazılır; login sonrası kullanıcı ChangeLog ekranına yönlendirilir.
|
||||
* Tetikleyici sürümdür, bundle değil: yalnızca yeni bir sürüm notu (git
|
||||
* 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
|
||||
* bildirim bileşenleri yalnızca gerçekten güncelleme varken lazy yüklenir.
|
||||
* Bu dosya entry chunk'ındadır: React/UI/store bağımlılığı yoktur, pencere
|
||||
* 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). */
|
||||
| 'completed'
|
||||
| 'activating'
|
||||
| 'reload-required'
|
||||
| 'error'
|
||||
|
||||
export type AppUpdateState = {
|
||||
status: AppUpdateStatus
|
||||
/** Pencere açık mı. */
|
||||
visible: boolean
|
||||
/** Sunucudaki (yeni) sürüm. */
|
||||
version?: string
|
||||
/** Çalışan bundle'ın sürümü. */
|
||||
/** Uygulamanın çalışan sürümü (backend'den gelir). */
|
||||
currentVersion: string
|
||||
changeLog: string[]
|
||||
files: UpdateFile[]
|
||||
|
|
@ -89,50 +90,34 @@ export type AppUpdateState = {
|
|||
dismissible: boolean
|
||||
}
|
||||
|
||||
export type Release = {
|
||||
version: string
|
||||
buildDate?: string
|
||||
commit?: string
|
||||
changeLog?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* `extraProperties.changeLogs` yükü: yalnızca ÇALIŞAN sürüm. Tam sürüm notu
|
||||
* listesi ChangeLog ekranının kendi endpoint'inden gelir, her config isteğinde
|
||||
* taşınmaz.
|
||||
*/
|
||||
export type VersionInfo = {
|
||||
buildId: string
|
||||
version: string
|
||||
buildTime?: string
|
||||
/** Uygulamanın (UI) çalışan sürümü. */
|
||||
uiVersion: string
|
||||
buildDate?: string
|
||||
commit?: string
|
||||
releases: Release[]
|
||||
/** Son sürümün notları. */
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 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. */
|
||||
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 ACTIVATION_RETRY_DELAY = 8_000
|
||||
const ACTIVATION_TIMEOUT = 45_000
|
||||
const REGISTRATION_TIMEOUT = 10_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. */
|
||||
const RETRY_DELAY_SECONDS = 15
|
||||
|
||||
const LAST_BUILD_KEY = 'sozsoft:last-build-id'
|
||||
const AUTO_RELOAD_GUARD_KEY = 'sozsoft:auto-reload-build'
|
||||
const LAST_VERSION_KEY = 'sozsoft:last-version'
|
||||
const PENDING_CHANGELOG_KEY = 'sozsoft:pending-changelog'
|
||||
const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload'
|
||||
|
||||
|
|
@ -170,7 +155,8 @@ const writeStorage = (key: string, value: string | undefined, session = false) =
|
|||
let state: AppUpdateState = {
|
||||
status: 'idle',
|
||||
visible: false,
|
||||
currentVersion: RUNNING_VERSION,
|
||||
// Backend'den (`setAppVersionInfo`) gelene kadar boş kalır.
|
||||
currentVersion: '',
|
||||
changeLog: [],
|
||||
files: [],
|
||||
loadedBytes: 0,
|
||||
|
|
@ -254,90 +240,43 @@ function recalculate(
|
|||
/* 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
|
||||
* kısa süre içinde (TTL) alınan sonuç paylaşılır. Böylece açılışta motor,
|
||||
* pencere ve ChangeLog ekranı tek isteği kullanır.
|
||||
* Store, config'i her yüklediğinde bunu çağırır; ayrı bir sürüm isteği yoktur.
|
||||
* Yük şekli: `extraProperties.changeLogs = { uiVersion, buildDate, tags[] }`.
|
||||
*/
|
||||
export const fetchVersionInfo = (force = false): Promise<VersionInfo | undefined> => {
|
||||
if (inFlightVersionRequest) return inFlightVersionRequest
|
||||
if (!force && versionInfoPromise && Date.now() - lastVersionFetchAt < VERSION_CACHE_TTL) {
|
||||
return versionInfoPromise
|
||||
}
|
||||
export const setAppVersionInfo = (raw: unknown) => {
|
||||
const data = raw as Partial<VersionInfo> | undefined
|
||||
if (!data?.uiVersion) return
|
||||
|
||||
inFlightVersionRequest = fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store' })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`version.json isteği ${response.status} döndü`)
|
||||
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
|
||||
})
|
||||
setState({
|
||||
currentVersion: data.uiVersion,
|
||||
changeLog: Array.isArray(data.tags) ? data.tags.filter(Boolean) : [],
|
||||
})
|
||||
|
||||
versionInfoPromise = inFlightVersionRequest
|
||||
return inFlightVersionRequest
|
||||
markVersionSeen(data.uiVersion)
|
||||
}
|
||||
|
||||
export const getRunningVersion = () => RUNNING_VERSION
|
||||
export const getLatestVersionInfo = () => latestInfo
|
||||
|
||||
/** Login sonrası changelog yönlendirmesi için bayrak. */
|
||||
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)
|
||||
|
||||
/**
|
||||
* Yeni bir build ilk kez çalıştığında (güncelleme sonrası ya da kullanıcı
|
||||
* uygulamaya deploy'dan sonra ilk kez girdiğinde) changelog bayrağını yazar.
|
||||
* Sürüm numarası son görülenden farklıysa changelog bayrağını yazar; kullanıcı
|
||||
* bir sonraki girişinde ChangeLog ekranına yönlendirilir.
|
||||
*
|
||||
* Tetikleyici bilerek BUNDLE DEĞİL, backend'den gelen sürümdür: yalnızca yeni
|
||||
* bir sürüm notu (git tag) eklendiğinde de — hiçbir bileşen değişmemiş olsa
|
||||
* bile — kullanıcı değişiklikleri görür. İlk açılışta (kayıt yokken) bayrak
|
||||
* yazılmaz, yalnızca mevcut sürüm not edilir.
|
||||
*/
|
||||
function markRunningBuildSeen(version: string) {
|
||||
const lastSeen = readStorage(LAST_BUILD_KEY)
|
||||
if (lastSeen && lastSeen !== RUNNING_BUILD_ID) {
|
||||
writeStorage(PENDING_CHANGELOG_KEY, version)
|
||||
}
|
||||
if (lastSeen !== RUNNING_BUILD_ID) writeStorage(LAST_BUILD_KEY, RUNNING_BUILD_ID)
|
||||
function markVersionSeen(version: string) {
|
||||
const lastSeen = readStorage(LAST_VERSION_KEY)
|
||||
if (lastSeen === version) return
|
||||
if (lastSeen) writeStorage(PENDING_CHANGELOG_KEY, version)
|
||||
writeStorage(LAST_VERSION_KEY, version)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -518,7 +457,7 @@ function activateWaitingWorker() {
|
|||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type SwFile = {
|
||||
index?: number
|
||||
index: number
|
||||
url: string
|
||||
name: string
|
||||
loaded: number
|
||||
|
|
@ -528,7 +467,7 @@ type SwFile = {
|
|||
|
||||
type SwProgressMessage = {
|
||||
type?: string
|
||||
/** Tam liste (START / DONE). */
|
||||
/** Tam liste (START / STATE / DONE). */
|
||||
files?: SwFile[]
|
||||
/** Yalnızca değişen dosyalar (PROGRESS). */
|
||||
updates?: SwFile[]
|
||||
|
|
@ -540,8 +479,8 @@ type SwProgressMessage = {
|
|||
}
|
||||
|
||||
const toFiles = (files: SwFile[] | undefined): UpdateFile[] =>
|
||||
(files ?? []).map((file, position) => ({
|
||||
index: file.index ?? position,
|
||||
(files ?? []).map((file) => ({
|
||||
index: file.index,
|
||||
url: file.url,
|
||||
name: file.name,
|
||||
loaded: file.loaded ?? 0,
|
||||
|
|
@ -625,8 +564,7 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
case 'SW_PRECACHE_PROGRESS':
|
||||
// Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır.
|
||||
if (!dialogMountRequested) void showUpdateDialog('downloading')
|
||||
// `files`: yalnızca eski service worker'lar (tam liste) için.
|
||||
recalculate(mergeFiles(toFiles(data.updates ?? data.files)), totals, {
|
||||
recalculate(mergeFiles(toFiles(data.updates)), totals, {
|
||||
status: 'downloading',
|
||||
visible: true,
|
||||
dismissible: !transitions,
|
||||
|
|
@ -696,64 +634,12 @@ async function showUpdateDialog(status: AppUpdateStatus) {
|
|||
/* 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ı?
|
||||
*
|
||||
* Dev'de karşılaştırma yapılmaz: `__APP_BUILD_ID__` vite config yüklenirken
|
||||
* gömüldüğü için `version.json` yeniden üretildiğinde sunucu yeniden
|
||||
* başlatılana kadar kalıcı olarak farklı görünür ve sonsuz yenileme oluşur.
|
||||
* 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
|
||||
* precache manifest'i de değişir. Bu yüzden ayrı bir sürüm dosyası yoklamaya
|
||||
* gerek yok; `registration.update()` yeni deploy'u tek başına yakalar.
|
||||
*/
|
||||
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) {
|
||||
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.
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = window.setInterval(() => {
|
||||
if (document.visibilityState === 'visible') void checkVersionEndpoint()
|
||||
if (document.visibilityState === 'visible') void runUpdateCheck()
|
||||
}, VERSION_POLL_INTERVAL)
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState !== 'visible') return
|
||||
void checkVersionEndpoint()
|
||||
void runUpdateCheck()
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
// Çevrimdışıyken beklemeye alınan güncelleme bağlantı gelince sürer.
|
||||
if (state.status === 'error') void retryUpdate()
|
||||
else void checkVersionEndpoint()
|
||||
else void runUpdateCheck()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -926,20 +812,11 @@ export const registerServiceWorker = () => {
|
|||
started = true
|
||||
scope.__sozsoftUpdateEngineStarted = true
|
||||
|
||||
// Sürüm okuma ve deploy kontrolü service worker'dan bağımsızdır; dev'de de
|
||||
// (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()
|
||||
}
|
||||
// Açılış performansı: yoklama dinleyicileri ilk boyamayı beklesin.
|
||||
const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, o?: object) => void })
|
||||
.requestIdleCallback
|
||||
if (idle) idle.call(window, boot, { timeout: 3_000 })
|
||||
else window.setTimeout(boot, 1_500)
|
||||
if (idle) idle.call(window, startWatchers, { timeout: 3_000 })
|
||||
else window.setTimeout(startWatchers, 1_500)
|
||||
|
||||
if (!('serviceWorker' in navigator)) return
|
||||
|
||||
|
|
@ -996,9 +873,6 @@ export const registerServiceWorker = () => {
|
|||
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
||||
setState({ status: state.status === 'idle' ? 'checking' : state.status })
|
||||
|
||||
const newDeploy = await checkVersionEndpoint(true)
|
||||
if (newDeploy) return 'updating'
|
||||
|
||||
if (!('serviceWorker' in navigator)) return 'unsupported'
|
||||
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 {
|
||||
readonly VITE_API_URL: string
|
||||
readonly VITE_CDN_URL: string
|
||||
readonly VITE_REACT_APP_VERSION: string
|
||||
readonly VITE_AI_URL: string
|
||||
readonly VITE_USE_POLLING?: string
|
||||
/** Dev sunucusunda service worker'ı açar (varsayılan kapalı). */
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { defineConfig, loadEnv } from 'vite'
|
||||
import type { Plugin } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
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 }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const buildInfo = readBuildInfo()
|
||||
const usePolling = env.VITE_USE_POLLING === 'true'
|
||||
// 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.
|
||||
|
|
@ -292,10 +272,6 @@ export default defineConfig(({ mode }) => {
|
|||
|
||||
define: {
|
||||
'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