diff --git a/.updateignore b/.updateignore
new file mode 100644
index 0000000..e85c356
--- /dev/null
+++ b/.updateignore
@@ -0,0 +1,41 @@
+# Dokumentacja (tylko wewnetrzna/deweloperska)
+*.md
+docs/
+CLAUDE.md
+AGENTS.md
+
+# Narzedzia deweloperskie
+.claude/
+.gitignore
+.git/
+tests/
+phpunit.xml
+phpunit.phar
+composer.json
+composer.lock
+vendor/
+test.ps1
+memory/
+
+# Infrastruktura aktualizacji (meta, nie runtime)
+updates/changelog.php
+updates/versions.php
+updates/install.php
+.updateignore
+build-update.ps1
+migrations/
+
+# Pliki konfiguracyjne klienta (wdrazane osobno)
+config.php
+.htaccess
+admin/.htaccess
+libraries/version.ini
+
+# Temp / cache / backups
+temp/
+backups/
+cache/
+cron/temp/
+
+# IDE
+.vscode/
diff --git a/TODO.md b/TODO.md
deleted file mode 100644
index 4f8aba1..0000000
--- a/TODO.md
+++ /dev/null
@@ -1,2 +0,0 @@
-1. Operacje na kombinacjach nie czyszczą cache
-2. Sprawdzić funkcję pod kątem SQL INJECTION
\ No newline at end of file
diff --git a/admin/templates/update/main-view.php b/admin/templates/update/main-view.php
index a6a8a76..cb20910 100644
--- a/admin/templates/update/main-view.php
+++ b/admin/templates/update/main-view.php
@@ -41,6 +41,17 @@
+ if ( !empty( $this->log ) ): ?>
+
Changelog
diff --git a/autoload/Domain/Update/UpdateRepository.php b/autoload/Domain/Update/UpdateRepository.php
index a3dc55c..ab7c701 100644
--- a/autoload/Domain/Update/UpdateRepository.php
+++ b/autoload/Domain/Update/UpdateRepository.php
@@ -61,10 +61,284 @@ class UpdateRepository
return [ 'success' => true, 'log' => $log, 'no_updates' => true ];
}
+ /**
+ * Dispatcher — próbuje pobrać manifest, jeśli jest → nowa ścieżka, jeśli brak → legacy.
+ */
private function downloadAndApply( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
+ $manifest = $this->downloadManifest( $baseUrl, $ver );
+
+ if ( $manifest !== null ) {
+ $log[] = '[INFO] Znaleziono manifest dla wersji ' . $ver;
+ return $this->downloadAndApplyWithManifest( $ver, $dir, $manifest, $log );
+ }
+
+ $log[] = '[INFO] Brak manifestu, używam trybu legacy';
+ return $this->downloadAndApplyLegacy( $ver, $dir, $log );
+ }
+
+ /**
+ * Pobiera manifest JSON dla danej wersji.
+ *
+ * @return array|null Zdekodowany manifest lub null jeśli brak
+ */
+ private function downloadManifest( string $baseUrl, string $ver )
+ {
+ $manifestUrl = $baseUrl . '/ver_' . $ver . '_manifest.json';
+
+ $ch = curl_init( $manifestUrl );
+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
+ curl_setopt( $ch, CURLOPT_HEADER, false );
+ curl_setopt( $ch, CURLOPT_TIMEOUT, 15 );
+ $response = curl_exec( $ch );
+ $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ curl_close( $ch );
+
+ if ( !$response || $httpCode !== 200 ) {
+ return null;
+ }
+
+ $manifest = json_decode( $response, true );
+ if ( !is_array( $manifest ) || !isset( $manifest['version'] ) ) {
+ return null;
+ }
+
+ return $manifest;
+ }
+
+ /**
+ * Aktualizacja z użyciem manifestu — checksum, backup, SQL z manifestu, usuwanie z manifestu.
+ */
+ private function downloadAndApplyWithManifest( string $ver, string $dir, array $manifest, array $log ): array
+ {
+ $baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
+
+ $log[] = '[INFO] Tryb aktualizacji: manifest';
+
+ // 1. Pobieranie ZIP
+ $zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
+ $log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;
+ $file = @file_get_contents( $zipUrl );
+
+ if ( $file === false ) {
+ $log[] = '[ERROR] Nie udało się pobrać pliku ZIP';
+ return [ 'success' => false, 'log' => $log ];
+ }
+
+ $fileSize = strlen( $file );
+ $log[] = '[OK] Pobrano plik ZIP, rozmiar: ' . $fileSize . ' bajtów';
+
+ if ( $fileSize < 100 ) {
+ $log[] = '[ERROR] Plik ZIP jest za mały (prawdopodobnie błąd pobierania)';
+ return [ 'success' => false, 'log' => $log ];
+ }
+
+ $dlHandler = @fopen( 'update.zip', 'w' );
+ if ( !$dlHandler ) {
+ $log[] = '[ERROR] Nie udało się otworzyć pliku update.zip do zapisu';
+ return [ 'success' => false, 'log' => $log ];
+ }
+ $written = fwrite( $dlHandler, $file );
+ fclose( $dlHandler );
+
+ if ( $written === false || $written === 0 ) {
+ $log[] = '[ERROR] Nie udało się zapisać pliku ZIP';
+ return [ 'success' => false, 'log' => $log ];
+ }
+
+ $log[] = '[OK] Zapisano plik ZIP (' . $written . ' bajtów)';
+
+ // 2. Weryfikacja checksum
+ if ( isset( $manifest['checksum_zip'] ) ) {
+ $checksumResult = $this->verifyChecksum( 'update.zip', $manifest['checksum_zip'], $log );
+ $log = $checksumResult['log'];
+
+ if ( !$checksumResult['valid'] ) {
+ @unlink( 'update.zip' );
+ return [ 'success' => false, 'log' => $log ];
+ }
+ }
+
+ // 3. Backup plików przed nadpisaniem
+ $log = $this->createBackup( $manifest, $log );
+
+ // 4. SQL z manifestu
+ if ( !empty( $manifest['sql'] ) ) {
+ $log[] = '[INFO] Wykonywanie zapytań SQL z manifestu (' . count( $manifest['sql'] ) . ')';
+ $success = 0;
+ $errors = 0;
+
+ foreach ( $manifest['sql'] as $query ) {
+ $query = trim( $query );
+ if ( $query !== '' ) {
+ if ( $this->db->query( $query ) ) {
+ $success++;
+ } else {
+ $errors++;
+ $log[] = '[WARNING] Błąd SQL: ' . $query;
+ }
+ }
+ }
+
+ $log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors;
+ }
+
+ // 5. Usuwanie plików z manifestu
+ if ( !empty( $manifest['files']['deleted'] ) ) {
+ $deletedCount = 0;
+ foreach ( $manifest['files']['deleted'] as $relativePath ) {
+ $fullPath = '../' . $relativePath;
+ if ( file_exists( $fullPath ) ) {
+ if ( @unlink( $fullPath ) ) {
+ $deletedCount++;
+ } else {
+ $log[] = '[WARNING] Nie udało się usunąć pliku: ' . $fullPath;
+ }
+ }
+ }
+ $log[] = '[INFO] Usunięto plików: ' . $deletedCount;
+ }
+
+ // 6. Usuwanie katalogów z manifestu
+ if ( !empty( $manifest['directories_deleted'] ) ) {
+ $deletedDirs = 0;
+ foreach ( $manifest['directories_deleted'] as $dirPath ) {
+ $fullPath = '../' . $dirPath;
+ if ( is_dir( $fullPath ) ) {
+ \Shared\Helpers\Helpers::delete_dir( $fullPath );
+ $deletedDirs++;
+ }
+ }
+ $log[] = '[INFO] Usunięto katalogów: ' . $deletedDirs;
+ }
+
+ // 7. Rozpakowywanie ZIP
+ $log = $this->extractZip( 'update.zip', $log );
+
+ // 8. Aktualizacja wersji
+ $versionFile = '../libraries/version.ini';
+ $handle = @fopen( $versionFile, 'w' );
+ if ( !$handle ) {
+ $log[] = '[ERROR] Nie udało się otworzyć pliku version.ini do zapisu';
+ return [ 'success' => false, 'log' => $log ];
+ }
+ fwrite( $handle, $ver );
+ fclose( $handle );
+
+ $log[] = '[OK] Zaktualizowano plik version.ini do wersji: ' . $ver;
+ $log[] = '[SUCCESS] Aktualizacja do wersji ' . $ver . ' zakończona pomyślnie';
+
+ return [ 'success' => true, 'log' => $log ];
+ }
+
+ /**
+ * Weryfikuje sumę kontrolną pliku.
+ *
+ * @param string $filePath Ścieżka do pliku
+ * @param string $expectedChecksum Suma w formacie "sha256:abc123..."
+ * @param array $log Tablica logów
+ * @return array{valid: bool, log: array}
+ */
+ private function verifyChecksum( string $filePath, string $expectedChecksum, array $log ): array
+ {
+ $parts = explode( ':', $expectedChecksum, 2 );
+ if ( count( $parts ) !== 2 ) {
+ $log[] = '[ERROR] Nieprawidłowy format sumy kontrolnej: ' . $expectedChecksum;
+ return [ 'valid' => false, 'log' => $log ];
+ }
+
+ $algorithm = $parts[0];
+ $expected = $parts[1];
+
+ $actual = @hash_file( $algorithm, $filePath );
+
+ if ( $actual === false ) {
+ $log[] = '[ERROR] Nie udało się obliczyć sumy kontrolnej pliku';
+ return [ 'valid' => false, 'log' => $log ];
+ }
+
+ if ( $actual !== $expected ) {
+ $log[] = '[ERROR] Suma kontrolna nie zgadza się! Oczekiwano: ' . $expected . ', otrzymano: ' . $actual;
+ return [ 'valid' => false, 'log' => $log ];
+ }
+
+ $log[] = '[OK] Suma kontrolna ZIP zgodna';
+ return [ 'valid' => true, 'log' => $log ];
+ }
+
+ /**
+ * Tworzy kopię zapasową plików przed aktualizacją.
+ *
+ * @param array $manifest Dane z manifestu
+ * @param array $log Tablica logów
+ * @return array Zaktualizowana tablica logów
+ */
+ private function createBackup( array $manifest, array $log ): array
+ {
+ $version = isset( $manifest['version'] ) ? $manifest['version'] : 'unknown';
+ $backupDir = '../backups/' . str_replace( '.', '_', $version ) . '_' . date( 'Ymd_His' );
+
+ $log[] = '[INFO] Tworzenie kopii zapasowej w: ' . $backupDir;
+
+ $projectRoot = realpath( '../' );
+ if ( !$projectRoot ) {
+ $log[] = '[WARNING] Nie udało się określić katalogu projektu, pomijam backup';
+ return $log;
+ }
+
+ $filesToBackup = [];
+ if ( isset( $manifest['files']['modified'] ) && is_array( $manifest['files']['modified'] ) ) {
+ $filesToBackup = array_merge( $filesToBackup, $manifest['files']['modified'] );
+ }
+ if ( isset( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) ) {
+ $filesToBackup = array_merge( $filesToBackup, $manifest['files']['deleted'] );
+ }
+
+ if ( empty( $filesToBackup ) ) {
+ $log[] = '[INFO] Brak plików do backupu';
+ return $log;
+ }
+
+ $backedUp = 0;
+ foreach ( $filesToBackup as $relativePath ) {
+ $sourcePath = $projectRoot . '/' . $relativePath;
+ if ( !file_exists( $sourcePath ) ) {
+ continue;
+ }
+
+ $targetPath = $backupDir . '/' . $relativePath;
+ $targetDir = dirname( $targetPath );
+
+ if ( !is_dir( $targetDir ) ) {
+ @mkdir( $targetDir, 0755, true );
+ }
+
+ if ( @copy( $sourcePath, $targetPath ) ) {
+ $backedUp++;
+ } else {
+ $log[] = '[WARNING] Nie udało się skopiować do backupu: ' . $relativePath;
+ }
+ }
+
+ $log[] = '[OK] Backup: skopiowano ' . $backedUp . ' plików';
+
+ @file_put_contents(
+ $backupDir . '/manifest.json',
+ json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE )
+ );
+
+ return $log;
+ }
+
+ /**
+ * Legacy — stary format aktualizacji (ZIP + _sql.txt + _files.txt).
+ */
+ private function downloadAndApplyLegacy( string $ver, string $dir, array $log ): array
+ {
+ $baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
+
// Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;
diff --git a/autoload/admin/Controllers/UpdateController.php b/autoload/admin/Controllers/UpdateController.php
index 6e6c77d..f3f7d53 100644
--- a/autoload/admin/Controllers/UpdateController.php
+++ b/autoload/admin/Controllers/UpdateController.php
@@ -14,9 +14,12 @@ class UpdateController
public function main_view(): string
{
+ $logContent = @file_get_contents( '../libraries/update_log.txt' );
+
return \Shared\Tpl\Tpl::view( 'update/main-view', [
'ver' => \Shared\Helpers\Helpers::get_version(),
'new_ver' => \Shared\Helpers\Helpers::get_new_version(),
+ 'log' => $logContent ?: '',
] );
}
diff --git a/build-update.ps1 b/build-update.ps1
new file mode 100644
index 0000000..eafad8f
--- /dev/null
+++ b/build-update.ps1
@@ -0,0 +1,388 @@
+<#
+.SYNOPSIS
+ Automatyczne budowanie paczki aktualizacji shopPRO na podstawie git diff miedzy tagami.
+
+.DESCRIPTION
+ Skrypt porownuje dwa tagi git, filtruje pliki przez .updateignore,
+ tworzy ZIP + manifest JSON i aktualizuje changelog.php oraz versions.php.
+
+.PARAMETER FromTag
+ Tag poczatkowy (np. v0.299). Domyslnie: ostatni tag.
+
+.PARAMETER ToTag
+ Tag docelowy (np. v0.300). Wymagany.
+
+.PARAMETER ChangelogEntry
+ Wpis do changelogu. Wymagany (chyba ze -DryRun).
+
+.PARAMETER DryRun
+ Tylko pokaz co zostaloby zrobione, bez tworzenia plikow.
+
+.EXAMPLE
+ ./build-update.ps1 -ToTag v0.300 -ChangelogEntry "NEW - Manifest-based update system"
+ ./build-update.ps1 -FromTag v0.299 -ToTag v0.300 -ChangelogEntry "NEW - opis" -DryRun
+#>
+
+param(
+ [string]$FromTag = "",
+ [Parameter(Mandatory=$true)]
+ [string]$ToTag,
+ [string]$ChangelogEntry = "",
+ [switch]$DryRun
+)
+
+$ErrorActionPreference = "Stop"
+
+# --- Helpers ---
+
+function Write-Step($msg) {
+ Write-Host " [*] $msg" -ForegroundColor Cyan
+}
+
+function Write-Ok($msg) {
+ Write-Host " [OK] $msg" -ForegroundColor Green
+}
+
+function Write-Warn($msg) {
+ Write-Host " [!] $msg" -ForegroundColor Yellow
+}
+
+function Write-Err($msg) {
+ Write-Host " [ERROR] $msg" -ForegroundColor Red
+}
+
+# --- 1. Walidacja tagow ---
+
+Write-Host "`n=== shopPRO Build Update ===" -ForegroundColor White
+
+if (-not $FromTag) {
+ $FromTag = (git describe --tags --abbrev=0 2>$null)
+ if (-not $FromTag) {
+ Write-Err "Nie znaleziono zadnego taga. Uzyj parametru -FromTag."
+ exit 1
+ }
+ Write-Step "Auto-detect FromTag: $FromTag"
+}
+
+# Sprawdz czy tagi istnieja
+$tagExists = git tag -l $FromTag
+if (-not $tagExists) {
+ Write-Err "Tag '$FromTag' nie istnieje."
+ exit 1
+}
+
+$toTagExists = git tag -l $ToTag
+if (-not $toTagExists) {
+ Write-Warn "Tag '$ToTag' nie istnieje. Uzywam HEAD jako punktu docelowego."
+ $diffTarget = "HEAD"
+} else {
+ $diffTarget = $ToTag
+}
+
+# --- 2. Wersja i katalog ---
+
+$versionNumber = $ToTag -replace '^v', ''
+$versionInt = [int]($versionNumber -replace '^0\.', '')
+
+# Oblicz katalog: 0.001-0.009 -> 0.00, 0.010-0.099 -> 0.00, 0.100-0.199 -> 0.10, 0.200-0.299 -> 0.20, 0.300-0.399 -> 0.30
+$dirTens = [math]::Floor($versionInt / 100)
+$dirStr = "0.{0}0" -f ($dirTens * 10).ToString().PadLeft(1, '0')
+
+# Format: jesli dirTens < 10 to "0.X0", np 0.00, 0.10, 0.20, 0.30
+if ($dirTens -lt 10) {
+ $dirStr = "0.{0}0" -f $dirTens.ToString().PadLeft(1, '0')
+} else {
+ $dirStr = "0.{0}0" -f $dirTens
+}
+
+Write-Step "Wersja: $versionNumber (int: $versionInt)"
+Write-Step "Katalog: updates/$dirStr/"
+
+# --- 3. Git diff ---
+
+Write-Step "Porownywanie: $FromTag..$diffTarget"
+
+$diffOutput = git diff --name-status "$FromTag..$diffTarget" 2>&1
+if ($LASTEXITCODE -ne 0) {
+ Write-Err "git diff nie powiodl sie: $diffOutput"
+ exit 1
+}
+
+$addedFiles = @()
+$modifiedFiles = @()
+$deletedFiles = @()
+$renamedFiles = @()
+
+foreach ($line in ($diffOutput -split "`n")) {
+ $line = $line.Trim()
+ if (-not $line) { continue }
+
+ $parts = $line -split "`t"
+ $status = $parts[0]
+ $filePath = if ($parts.Count -gt 1) { $parts[1] } else { "" }
+
+ # Zamien backslash na slash
+ $filePath = $filePath -replace '\\', '/'
+
+ switch -Wildcard ($status) {
+ "A" { $addedFiles += $filePath }
+ "M" { $modifiedFiles += $filePath }
+ "D" { $deletedFiles += $filePath }
+ "R*" {
+ # Rename: stary plik = deleted, nowy = added
+ $newPath = if ($parts.Count -gt 2) { $parts[2] -replace '\\', '/' } else { "" }
+ $deletedFiles += $filePath
+ if ($newPath) { $addedFiles += $newPath }
+ }
+ }
+}
+
+Write-Step "Zmiany: A=$($addedFiles.Count), M=$($modifiedFiles.Count), D=$($deletedFiles.Count)"
+
+# --- 4. Filtrowanie przez .updateignore ---
+
+$ignorePatterns = @()
+$ignoreFile = ".updateignore"
+
+if (Test-Path $ignoreFile) {
+ $ignorePatterns = Get-Content $ignoreFile | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '^\s*#') }
+ Write-Step "Zaladowano $($ignorePatterns.Count) wzorcow z .updateignore"
+}
+
+function Test-Ignored {
+ param([string]$FilePath)
+
+ foreach ($pattern in $ignorePatterns) {
+ # Wzorzec katalogu (konczy sie na /)
+ if ($pattern.EndsWith('/')) {
+ $dirPattern = $pattern.TrimEnd('/')
+ if ($FilePath -like "$dirPattern/*" -or $FilePath -eq $dirPattern) {
+ return $true
+ }
+ }
+ # Wzorzec z wildcard
+ elseif ($pattern.Contains('*')) {
+ # *.md -> dopasuj nazwe pliku
+ if ($pattern.StartsWith('*')) {
+ $fileName = Split-Path $FilePath -Leaf
+ if ($fileName -like $pattern) { return $true }
+ }
+ # Zwykly glob
+ elseif ($FilePath -like $pattern) { return $true }
+ }
+ # Dokladne dopasowanie
+ else {
+ if ($FilePath -eq $pattern) { return $true }
+ }
+ }
+ return $false
+}
+
+$filteredAdded = @()
+$filteredModified = @()
+$filteredDeleted = @()
+$ignoredCount = 0
+
+foreach ($f in $addedFiles) {
+ if (Test-Ignored $f) { $ignoredCount++; continue }
+ $filteredAdded += $f
+}
+foreach ($f in $modifiedFiles) {
+ if (Test-Ignored $f) { $ignoredCount++; continue }
+ $filteredModified += $f
+}
+foreach ($f in $deletedFiles) {
+ if (Test-Ignored $f) { $ignoredCount++; continue }
+ $filteredDeleted += $f
+}
+
+Write-Step "Po filtrowaniu: A=$($filteredAdded.Count), M=$($filteredModified.Count), D=$($filteredDeleted.Count) (pominieto: $ignoredCount)"
+
+# Rozdziel usuniete pliki i katalogi
+$deletedDirs = @()
+$deletedFilesOnly = @()
+
+foreach ($f in $filteredDeleted) {
+ # Sprawdz czy to byl katalog (w git nie ma katalogow, ale mozemy sprawdzic po wzorcu)
+ # Git nie trackuje pustych katalogow, wiec deleted entries to zawsze pliki
+ $deletedFilesOnly += $f
+}
+
+# --- 5. Odczyt migracji SQL ---
+
+$sqlQueries = @()
+$migrationFile = "migrations/$versionNumber.sql"
+
+if (Test-Path $migrationFile) {
+ $sqlQueries = Get-Content $migrationFile | Where-Object { $_.Trim() -ne '' }
+ Write-Step "Znaleziono migracje SQL: $migrationFile ($($sqlQueries.Count) zapytan)"
+} else {
+ Write-Step "Brak migracji SQL ($migrationFile nie istnieje)"
+}
+
+# --- 6. Podsumowanie ---
+
+$filesToPack = $filteredAdded + $filteredModified
+
+if ($filesToPack.Count -eq 0 -and $filteredDeleted.Count -eq 0 -and $sqlQueries.Count -eq 0) {
+ Write-Warn "Brak zmian do pakowania (po filtrowaniu). Przerywam."
+ exit 0
+}
+
+Write-Host "`n--- Pliki do paczki ---" -ForegroundColor White
+foreach ($f in $filteredAdded) { Write-Host " A $f" -ForegroundColor Green }
+foreach ($f in $filteredModified) { Write-Host " M $f" -ForegroundColor Yellow }
+foreach ($f in $filteredDeleted) { Write-Host " D $f" -ForegroundColor Red }
+
+if ($sqlQueries.Count -gt 0) {
+ Write-Host "`n--- SQL ---" -ForegroundColor White
+ foreach ($q in $sqlQueries) { Write-Host " $q" -ForegroundColor Gray }
+}
+
+# --- DryRun? ---
+
+if ($DryRun) {
+ Write-Host "`n[DRY RUN] Zadne pliki nie zostaly utworzone.`n" -ForegroundColor Magenta
+ exit 0
+}
+
+# --- 7. Walidacja ChangelogEntry ---
+
+if (-not $ChangelogEntry) {
+ Write-Err "Parametr -ChangelogEntry jest wymagany (chyba ze uzywasz -DryRun)."
+ exit 1
+}
+
+# --- 8. Tworzenie temp i kopiowanie plikow ---
+
+$tempDir = "temp/temp_$versionInt"
+
+if (Test-Path $tempDir) {
+ Remove-Item -Recurse -Force $tempDir
+}
+
+foreach ($f in $filesToPack) {
+ $destPath = Join-Path $tempDir $f
+ $destDir = Split-Path $destPath -Parent
+
+ if (-not (Test-Path $destDir)) {
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ }
+
+ if (Test-Path $f) {
+ Copy-Item $f $destPath -Force
+ } else {
+ Write-Warn "Plik nie istnieje (moze zostal usuniety po TAGU): $f"
+ }
+}
+
+Write-Ok "Skopiowano $($filesToPack.Count) plikow do $tempDir"
+
+# --- 9. Tworzenie ZIP ---
+
+$updatesDir = "updates/$dirStr"
+if (-not (Test-Path $updatesDir)) {
+ New-Item -ItemType Directory -Path $updatesDir -Force | Out-Null
+}
+
+$zipPath = "$updatesDir/ver_$versionNumber.zip"
+
+if (Test-Path $zipPath) {
+ Remove-Item $zipPath -Force
+}
+
+# Pakuj zawartosc temp dir (bez folderu temp/)
+$originalLocation = Get-Location
+Set-Location $tempDir
+Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force
+Set-Location $originalLocation
+
+Write-Ok "Utworzono ZIP: $zipPath"
+
+# --- 10. Checksum SHA256 ---
+
+$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower()
+Write-Ok "SHA256: $hash"
+
+# --- 11. Manifest JSON ---
+
+$manifest = @{
+ version = $versionNumber
+ date = (Get-Date -Format "yyyy-MM-dd")
+ checksum_zip = "sha256:$hash"
+ files = @{
+ modified = $filteredModified
+ added = $filteredAdded
+ deleted = $deletedFilesOnly
+ }
+ directories_deleted = $deletedDirs
+ sql = $sqlQueries
+ changelog = $ChangelogEntry
+}
+
+$manifestJson = $manifest | ConvertTo-Json -Depth 4
+$manifestPath = "$updatesDir/ver_${versionNumber}_manifest.json"
+$manifestJson | Out-File $manifestPath -Encoding UTF8
+
+Write-Ok "Utworzono manifest: $manifestPath"
+
+# --- 12. Legacy _sql.txt i _files.txt (okres przejsciowy) ---
+
+if ($sqlQueries.Count -gt 0) {
+ $sqlPath = "$updatesDir/ver_${versionNumber}_sql.txt"
+ ($sqlQueries -join "`n") | Out-File $sqlPath -Encoding UTF8 -NoNewline
+ Write-Ok "Utworzono legacy SQL: $sqlPath"
+}
+
+if ($deletedFilesOnly.Count -gt 0 -or $deletedDirs.Count -gt 0) {
+ $filesContent = @()
+ foreach ($f in $deletedFilesOnly) { $filesContent += "F: ../$f" }
+ foreach ($d in $deletedDirs) { $filesContent += "D: ../$d" }
+
+ $filesPath = "$updatesDir/ver_${versionNumber}_files.txt"
+ ($filesContent -join "`n") | Out-File $filesPath -Encoding UTF8 -NoNewline
+ Write-Ok "Utworzono legacy files: $filesPath"
+}
+
+# --- 13. Aktualizacja versions.php ---
+
+$versionsFile = "updates/versions.php"
+if (Test-Path $versionsFile) {
+ $content = Get-Content $versionsFile -Raw
+ $content = $content -replace '\$current_ver\s*=\s*\d+;', "`$current_ver = $versionInt;"
+ $content | Out-File $versionsFile -Encoding UTF8 -NoNewline
+ Write-Ok "Zaktualizowano versions.php: `$current_ver = $versionInt"
+}
+
+# --- 14. Aktualizacja changelog.php ---
+
+$changelogFile = "updates/changelog.php"
+if (Test-Path $changelogFile) {
+ $dateStr = Get-Date -Format "dd.MM.yyyy"
+ $newEntry = "ver. $versionNumber - $dateStr
`n$ChangelogEntry`n
`n"
+
+ $changelogContent = Get-Content $changelogFile -Raw
+ $changelogContent = $newEntry + $changelogContent
+ $changelogContent | Out-File $changelogFile -Encoding UTF8 -NoNewline
+ Write-Ok "Zaktualizowano changelog.php"
+}
+
+# --- 15. Cleanup ---
+
+if (Test-Path $tempDir) {
+ Remove-Item -Recurse -Force $tempDir
+}
+# Usun pusty folder temp jesli nie ma juz w nim nic
+if ((Test-Path "temp") -and ((Get-ChildItem "temp" -Force).Count -eq 0)) {
+ Remove-Item "temp" -Force
+}
+
+Write-Ok "Wyczyszczono pliki tymczasowe"
+
+# --- Podsumowanie ---
+
+Write-Host "`n=== Gotowe ===" -ForegroundColor Green
+Write-Host " ZIP: $zipPath"
+Write-Host " Manifest: $manifestPath"
+Write-Host " Wersja: $versionNumber (int: $versionInt)"
+Write-Host ""
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 8c49c4e..7233535 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,6 +4,19 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
+## ver. 0.300 (2026-02-21) - System aktualizacji oparty na manifestach JSON
+
+- **NEW**: Manifest JSON per wersja — zastępuje osobne pliki `_sql.txt` i `_files.txt`
+- **NEW**: Weryfikacja checksum SHA256 pobranych paczek ZIP
+- **NEW**: Automatyczny backup plików przed nadpisaniem (`backups/` directory)
+- **NEW**: `build-update.ps1` — automatyczne budowanie paczek z `git diff` między tagami
+- **NEW**: `.updateignore` — wzorce plików wykluczonych z paczek aktualizacji
+- **NEW**: `migrations/` — folder na wersjonowane pliki SQL
+- **NEW**: Panel "Log ostatniej aktualizacji" w widoku aktualizacji admina
+- **UPDATE**: `UpdateRepository` — dual-mode dispatcher (manifest + legacy fallback)
+
+---
+
## ver. 0.299 (2026-02-21) - Widoczność kolumn w tabelach
- **NEW**: Toggle widoczności kolumn w komponentach `table-list` — przycisk z ikoną kolumn, dropdown z toggle switchami
diff --git a/docs/TESTING.md b/docs/TESTING.md
index d742d2b..fb05cdc 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -23,10 +23,10 @@ composer test # standard
## Aktualny stan
```text
-OK (687 tests, 1971 assertions)
+OK (692 tests, 1988 assertions)
```
-Zweryfikowano: 2026-02-19 (ver. 0.297)
+Zweryfikowano: 2026-02-21 (ver. 0.300)
## Konfiguracja
@@ -66,6 +66,7 @@ tests/
| | |-- Settings/SettingsRepositoryTest.php
| | |-- ShopStatus/ShopStatusRepositoryTest.php
| | |-- Transport/TransportRepositoryTest.php
+| | |-- Update/UpdateRepositoryTest.php
| | `-- User/UserRepositoryTest.php
| `-- admin/
| `-- Controllers/
diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md
index d67b986..a155334 100644
--- a/docs/UPDATE_INSTRUCTIONS.md
+++ b/docs/UPDATE_INSTRUCTIONS.md
@@ -1,136 +1,113 @@
# Instrukcja tworzenia aktualizacji shopPRO
-## Struktura aktualizacji
+## Nowy sposób (od v0.301) — automatyczny build script
+
+### Wymagania
+- Git z tagami wersji (np. `v0.299`, `v0.300`)
+- PowerShell
+
+### Workflow
+
+```
+1. Pracuj normalnie: commit, push, commit, push...
+2. Gdy wersja gotowa:
+ → git tag v0.XXX
+ → ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis"
+3. Upload plików z updates/0.XX/ na serwer aktualizacji
+```
+
+### Użycie build-update.ps1
+
+```powershell
+# Podgląd zmian (bez tworzenia plików)
+./build-update.ps1 -ToTag v0.301 -DryRun
+
+# Budowanie paczki (auto-detect poprzedniego tagu)
+./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany"
+
+# Z jawnym tagiem źródłowym
+./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis"
+```
+
+### Co robi skrypt automatycznie
+1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików
+2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.)
+3. Kopiowanie plików do temp, tworzenie ZIP
+4. SHA256 checksum ZIP-a
+5. Generowanie `ver_X.XXX_manifest.json`
+6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy)
+7. Aktualizacja `versions.php` i `changelog.php`
+8. Cleanup
+
+### Pliki wynikowe
+- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami
+- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL
+- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy)
+- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy)
+
+### Migracje SQL
+Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`).
+Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`.
+
+### Format manifestu
+```json
+{
+ "version": "0.301",
+ "date": "2026-02-22",
+ "checksum_zip": "sha256:abc123...",
+ "files": {
+ "modified": ["autoload/Domain/Order/OrderRepository.php"],
+ "added": ["autoload/Domain/Order/NewHelper.php"],
+ "deleted": ["autoload/shop/OldClass.php"]
+ },
+ "directories_deleted": [],
+ "sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"],
+ "changelog": "NEW - opis zmiany"
+}
+```
+
+### .updateignore
+Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`).
+
+---
+
+## Stary sposób (do v0.300) — ręczne pakowanie
+
+### Struktura aktualizacji
Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesiątki wersji.
-### Pliki aktualizacji:
+#### Pliki aktualizacji:
- `ver_X.XXX.zip` - paczka ZIP ze zmienionymi plikami (BEZ folderu wersji, bezpośrednio struktura katalogów)
- `ver_X.XXX_sql.txt` - opcjonalny plik z zapytaniami SQL (jeśli wymagane zmiany w bazie)
- `ver_X.XXX_files.txt` - opcjonalny plik z listą plików do **USUNIĘCIA** przy aktualizacji (format: `F: ../sciezka/do/pliku.php`)
- `changelog.php` - historia zmian
- `versions.php` - konfiguracja wersji (zmienna `$current_ver`)
-### Zasada pakowania plików
+#### Zasada pakowania plików
- Do paczek aktualizacji **nie dodajemy plików `*.md`** (dokumentacja jest tylko wewnętrzna/deweloperska).
- Do paczek aktualizacji **nie dodajemy `updates/changelog.php`** (to plik serwisowy po stronie repozytorium aktualizacji, nie runtime klienta).
- Do paczek aktualizacji **nie dodajemy głównego `.htaccess` z katalogu projektu** (ten plik wdrażamy osobno, poza ZIP aktualizacji).
-## Procedura tworzenia nowej aktualizacji
+### Procedura ręczna
-## Status biezacej aktualizacji (ver. 0.299)
+1. Określ numer wersji
+2. Utwórz folder tymczasowy: `mkdir -p temp/temp_XXX/sciezka/do/pliku`
+3. Skopiuj zmienione pliki do folderu tymczasowego
+4. Utwórz ZIP z zawartości folderu (nie z samego folderu!)
+5. Usuń folder tymczasowy
+6. Zaktualizuj `changelog.php` i `versions.php`
+7. (Opcjonalnie) Utwórz `_sql.txt` i `_files.txt`
-- Wersja udostepniona: `0.299` (data: 2026-02-21).
+**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji. Struktura ZIP zaczyna się bezpośrednio od katalogów projektu (admin/, autoload/, itp.).
+
+## Status bieżącej aktualizacji (ver. 0.300)
+
+- Wersja udostępniona: `0.300` (data: 2026-02-21).
- Pliki publikacyjne:
- - `updates/0.20/ver_0.299.zip`
+ - `updates/0.30/ver_0.300.zip`
- Pliki metadanych aktualizacji:
- `updates/changelog.php`
- - `updates/versions.php` (`$current_ver = 299`)
-- Weryfikacja testow przed publikacja:
- - `OK (687 tests, 1971 assertions)`
-
-### 1. Określ numer wersji
-Sprawdź ostatnią wersję w `updates/` i zwiększ o 1.
-
-### 2. Utwórz folder tymczasowy ze strukturą w katalogu temp
-```bash
-mkdir -p temp/temp_XXX/sciezka/do/pliku
-```
-
-**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji (np. ver_0.234/).
-Struktura ZIP powinna zaczynać się bezpośrednio od katalogów projektu (admin/, autoload/, itp.).
-
-### 3. Skopiuj zmienione pliki do folderu tymczasowego
-```bash
-cp sciezka/do/pliku.php temp/temp_XXX/sciezka/do/pliku.php
-```
-
-### 4. Utwórz plik ZIP z zawartości folderu (nie z samego folderu!)
-```powershell
-cd temp/temp_XXX
-powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_X.XXX.zip' -Force"
-```
-
-### 5. Usuń folder tymczasowy
-```bash
-rm -rf temp/temp_XXX
-```
-
-### 6. Zaktualizuj changelog.php
-Dodaj wpis na początku pliku:
-```html
-ver. X.XXX - DD.MM.YYYY
-- NEW/FIX/UPDATE - opis zmiany
-
-```
-
-Prefiksy:
-- `NEW` - nowa funkcjonalność
-- `FIX` - naprawa błędu
-- `UPDATE` - aktualizacja istniejącej funkcjonalności
-
-### 7. Zaktualizuj versions.php
-Zmień wartość `$current_ver` na nowy numer wersji (bez przedrostka 0.):
-```php
-$current_ver = 234; // dla wersji 0.234
-```
-
-### 8. (Opcjonalnie) Utwórz plik SQL
-Jeśli aktualizacja wymaga zmian w bazie danych, utwórz plik `ver_X.XXX_sql.txt` z zapytaniami SQL.
-
-### 9. (Opcjonalnie) Utwórz plik z listą plików do usunięcia
-Jeśli aktualizacja wymaga usunięcia przestarzałych plików, utwórz plik `ver_X.XXX_files.txt`:
-```
-F: ../sciezka/do/pliku1.php
-F: ../sciezka/do/pliku2.php
-```
-**UWAGA:** Pliki wymienione w tym pliku zostaną USUNIĘTE z systemu podczas aktualizacji.
-
-## Przykład - aktualizacja 0.234
-
-Zmienione pliki:
-- `autoload/admin/controls/class.ShopOrder.php`
-- `admin/templates/shop-order/order-details.php`
-
-Opis: Dodanie przycisku do zaznaczania zamówienia jako wysłane do trustmate.io
-
-### Komendy:
-
-```bash
-# Utwórz strukturę w folderze tymczasowym
-mkdir -p temp/temp_234/autoload/admin/controls
-mkdir -p temp/temp_234/admin/templates/shop-order
-
-# Skopiuj pliki
-cp autoload/admin/controls/class.ShopOrder.php temp/temp_234/autoload/admin/controls/
-cp admin/templates/shop-order/order-details.php temp/temp_234/admin/templates/shop-order/
-
-# Utwórz ZIP z ZAWARTOŚCI folderu (ważne: wejdź do folderu i spakuj '*')
-cd temp/temp_234
-powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_0.234.zip' -Force"
-
-# Wróć i usuń folder tymczasowy
-cd ..
-rm -rf temp_234
-```
-
-### Poprawna struktura ZIP:
-```
-ver_0.234.zip
-├── admin/
-│ └── templates/
-│ └── shop-order/
-│ └── order-details.php
-└── autoload/
- └── admin/
- └── controls/
- └── class.ShopOrder.php
-```
-
-### NIEPOPRAWNA struktura (do uniknięcia):
-```
-ver_0.234.zip
-└── ver_0.234/ <-- tego folderu NIE powinno być!
- ├── admin/
- └── autoload/
-```
+ - `updates/versions.php` (`$current_ver = 300`)
+- Weryfikacja testów przed publikacją:
+ - `OK (692 tests, 1988 assertions)`
diff --git a/changes b/migrations/.gitkeep
similarity index 100%
rename from changes
rename to migrations/.gitkeep
diff --git a/test-debug.bat b/test-debug.bat
deleted file mode 100644
index 7a8da3c..0000000
--- a/test-debug.bat
+++ /dev/null
@@ -1,13 +0,0 @@
-@echo off
-REM Skrypt do uruchamiania testów z PEŁNYMI szczegółami
-
-echo.
-echo ================================
-echo Testy DEBUG - pełne szczegóły
-echo ================================
-echo.
-
-C:\xampp\php\php.exe phpunit.phar --debug %*
-
-echo.
-pause
diff --git a/test-simple.bat b/test-simple.bat
deleted file mode 100644
index 6277f71..0000000
--- a/test-simple.bat
+++ /dev/null
@@ -1,13 +0,0 @@
-@echo off
-REM Skrypt do uruchamiania testów - tylko kropki
-
-echo.
-echo ================================
-echo Testy jednostkowe shopPRO
-echo ================================
-echo.
-
-C:\xampp\php\php.exe phpunit.phar %*
-
-echo.
-pause
diff --git a/test.bat b/test.bat
deleted file mode 100644
index 4708548..0000000
--- a/test.bat
+++ /dev/null
@@ -1,13 +0,0 @@
-@echo off
-REM Skrypt do uruchamiania testów PHPUnit
-
-echo.
-echo ================================
-echo Testy jednostkowe shopPRO
-echo ================================
-echo.
-
-C:\xampp\php\php.exe phpunit.phar --testdox %*
-
-echo.
-pause
diff --git a/test.ps1 b/test.ps1
deleted file mode 100644
index 66f3152..0000000
--- a/test.ps1
+++ /dev/null
@@ -1,47 +0,0 @@
-Param(
- [Parameter(ValueFromRemainingArguments = $true)]
- [string[]]$PhpUnitArgs
-)
-
-$ErrorActionPreference = "Stop"
-
-function Resolve-PhpExe {
- $cmd = Get-Command php -ErrorAction SilentlyContinue
- if ($cmd -and $cmd.Source) {
- return $cmd.Source
- }
-
- $candidates = @(
- "C:\xampp\php\php.exe",
- "C:\php\php.exe",
- "C:\Program Files\PHP\php.exe"
- )
-
- foreach ($candidate in $candidates) {
- if (Test-Path $candidate) {
- return $candidate
- }
- }
-
- throw "Nie znaleziono interpretera PHP. Dodaj php do PATH albo zainstaluj PHP (np. XAMPP)."
-}
-
-$phpExe = Resolve-PhpExe
-$phpUnitPhar = Join-Path $PSScriptRoot "phpunit.phar"
-
-if (-not (Test-Path $phpUnitPhar)) {
- throw "Brak pliku phpunit.phar w katalogu projektu: $PSScriptRoot"
-}
-
-$args = @($phpUnitPhar, "--do-not-cache-result") + $PhpUnitArgs
-
-Write-Host ""
-Write-Host "================================"
-Write-Host " Testy jednostkowe shopPRO"
-Write-Host "================================"
-Write-Host "PHP: $phpExe"
-Write-Host "Cmd: $phpExe $($args -join ' ')"
-Write-Host ""
-
-& $phpExe @args
-exit $LASTEXITCODE
diff --git a/test.sh b/test.sh
deleted file mode 100644
index ab3fc7f..0000000
--- a/test.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-# Skrypt do uruchamiania testów PHPUnit
-
-echo ""
-echo "================================"
-echo " Testy jednostkowe shopPRO"
-echo "================================"
-echo ""
-
-/c/xampp/php/php.exe phpunit.phar "$@"
diff --git a/tests/Unit/Domain/Update/UpdateRepositoryTest.php b/tests/Unit/Domain/Update/UpdateRepositoryTest.php
index 8aac25a..5a21841 100644
--- a/tests/Unit/Domain/Update/UpdateRepositoryTest.php
+++ b/tests/Unit/Domain/Update/UpdateRepositoryTest.php
@@ -50,7 +50,7 @@ class UpdateRepositoryTest extends TestCase
$repository = new UpdateRepository($db);
$repository->runPendingMigrations();
- $this->assertTrue(true); // No exception thrown
+ $this->assertTrue(true);
}
public function testHasPrivateHelperMethods(): void
@@ -59,6 +59,11 @@ class UpdateRepositoryTest extends TestCase
$privateMethods = [
'downloadAndApply',
+ 'downloadAndApplyLegacy',
+ 'downloadAndApplyWithManifest',
+ 'downloadManifest',
+ 'verifyChecksum',
+ 'createBackup',
'executeSql',
'deleteFiles',
'extractZip',
@@ -77,4 +82,99 @@ class UpdateRepositoryTest extends TestCase
);
}
}
+
+ public function testVerifyChecksumValidFormat(): void
+ {
+ $db = $this->createMockDb();
+ $repository = new UpdateRepository($db);
+
+ $reflection = new \ReflectionClass($repository);
+ $method = $reflection->getMethod('verifyChecksum');
+ $method->setAccessible(true);
+
+ // Create a temp file with known content
+ $tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_');
+ file_put_contents($tmpFile, 'test content for checksum');
+
+ $expectedHash = hash_file('sha256', $tmpFile);
+ $result = $method->invoke($repository, $tmpFile, 'sha256:' . $expectedHash, []);
+
+ $this->assertTrue($result['valid']);
+ $this->assertNotEmpty($result['log']);
+
+ @unlink($tmpFile);
+ }
+
+ public function testVerifyChecksumInvalidHash(): void
+ {
+ $db = $this->createMockDb();
+ $repository = new UpdateRepository($db);
+
+ $reflection = new \ReflectionClass($repository);
+ $method = $reflection->getMethod('verifyChecksum');
+ $method->setAccessible(true);
+
+ $tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_');
+ file_put_contents($tmpFile, 'test content');
+
+ $result = $method->invoke($repository, $tmpFile, 'sha256:invalidhash', []);
+
+ $this->assertFalse($result['valid']);
+
+ @unlink($tmpFile);
+ }
+
+ public function testVerifyChecksumInvalidFormat(): void
+ {
+ $db = $this->createMockDb();
+ $repository = new UpdateRepository($db);
+
+ $reflection = new \ReflectionClass($repository);
+ $method = $reflection->getMethod('verifyChecksum');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($repository, '/tmp/nonexistent', 'badformat', []);
+
+ $this->assertFalse($result['valid']);
+ }
+
+ public function testCreateBackupWithEmptyManifest(): void
+ {
+ $db = $this->createMockDb();
+ $repository = new UpdateRepository($db);
+
+ $reflection = new \ReflectionClass($repository);
+ $method = $reflection->getMethod('createBackup');
+ $method->setAccessible(true);
+
+ $manifest = [
+ 'version' => '0.999',
+ 'files' => [],
+ ];
+
+ $log = $method->invoke($repository, $manifest, []);
+
+ $this->assertIsArray($log);
+ $hasBackupInfo = false;
+ foreach ($log as $entry) {
+ if (strpos($entry, 'Brak plików do backupu') !== false) {
+ $hasBackupInfo = true;
+ }
+ }
+ $this->assertTrue($hasBackupInfo);
+ }
+
+ public function testDownloadManifestReturnsNullForInvalidUrl(): void
+ {
+ $db = $this->createMockDb();
+ $repository = new UpdateRepository($db);
+
+ $reflection = new \ReflectionClass($repository);
+ $method = $reflection->getMethod('downloadManifest');
+ $method->setAccessible(true);
+
+ $result = $method->invoke($repository, 'http://invalid.nonexistent.test', '0.999');
+
+ $this->assertNull($result);
+ }
}
diff --git a/updates/0.30/ver_0.300.zip b/updates/0.30/ver_0.300.zip
new file mode 100644
index 0000000..6155f1b
Binary files /dev/null and b/updates/0.30/ver_0.300.zip differ
diff --git a/updates/changelog.php b/updates/changelog.php
index 702735d..d98e044 100644
--- a/updates/changelog.php
+++ b/updates/changelog.php
@@ -1,3 +1,7 @@
+ver. 0.300 - 21.02.2026
+- NEW - System aktualizacji oparty na manifestach JSON (checksum SHA256, backup plików, automatyczny build)
+- NEW - Panel logu aktualizacji w panelu admina
+
ver. 0.299 - 21.02.2026
- NEW - Ukrywanie/pokazywanie kolumn w tabelach admina (toggle switch + localStorage)
diff --git a/updates/versions.php b/updates/versions.php
index 168f4a3..4d99971 100644
--- a/updates/versions.php
+++ b/updates/versions.php
@@ -1,5 +1,5 @@
-$current_ver = 299;
+$current_ver = 300;
for ($i = 1; $i <= $current_ver; $i++)
{