docs: plan implementacji modulu Releases (dwukanalowy system aktualizacji)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 00:26:09 +01:00
parent ff227fa6e0
commit 5e6c3e46fc

View File

@@ -0,0 +1,766 @@
# Releases Module Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Wdrożyć dwukanałowy system aktualizacji (beta/stable) z zarządzaniem licencjami w panelu admina.
**Architecture:** Dwie nowe tabele MySQL (`pp_update_licenses`, `pp_update_versions`) dostępne tylko na serwerze dewelopera. `updates/versions.php` czyta z DB przez Medoo. Nowy moduł `Releases` w panelu admina zarządza wersjami i licencjami. Całość wykluczona z paczek klientów przez `.updateignore`.
**Tech Stack:** PHP 8.x, Medoo 1.x (`$mdb` global), jQuery UI (dialog), Bootstrap 3 (klasy CSS), `\Tpl` (admin templates z `admin/templates/`), `\S::get()` (POST→GET params).
---
### Task 1: Dodaj wykluczenia do `.updateignore`
**Files:**
- Modify: `.updateignore`
**Step 1: Dopisz nowe wykluczenia**
Otwórz `.updateignore` i dopisz na końcu:
```
# Moduł zarządzania releaseami (tylko serwer dewelopera)
autoload/admin/controls/class.Releases.php
autoload/admin/factory/class.Releases.php
autoload/admin/view/class.Releases.php
admin/templates/releases/
# Menu dewelopera
templates/additional-menu.php
```
**Step 2: Zweryfikuj manualnie**
Uruchom build w trybie dry-run i sprawdź, że powyższe pliki NIE pojawiają się na liście:
```powershell
./build-update.ps1 -ToTag v9.999 -DryRun
```
**Step 3: Commit**
```bash
git add .updateignore
git commit -m "chore: wyklucz modul Releases i menu dewelopera z paczek klientow"
```
---
### Task 2: Utwórz tabele DB (jednorazowo na serwerze)
**Files:**
- Create: `_db_releases_setup.sql` (uruchom raz w phpMyAdmin, nie commituj)
**Step 1: Utwórz plik SQL**
```sql
CREATE TABLE pp_update_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
`key` VARCHAR(64) NOT NULL UNIQUE,
domain VARCHAR(255) NOT NULL,
valid_to_date DATE NULL,
valid_to_version VARCHAR(10) NULL,
beta TINYINT(1) NOT NULL DEFAULT 0,
note TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE pp_update_versions (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(10) NOT NULL UNIQUE,
channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
created_at DATETIME NULL,
promoted_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
**Step 2: Uruchom w phpMyAdmin na serwerze `cmspro.project-dc.pl`**
Wklej SQL do phpMyAdmin → Execute. Obie tabele muszą być widoczne.
**Step 3: Nie commituj** — ten plik jest tylko pomocniczy, możesz go usunąć po wykonaniu.
---
### Task 3: Utwórz `admin\factory\Releases`
**Files:**
- Create: `autoload/admin/factory/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\factory;
class Releases
{
public static function get_versions(): array
{
global $mdb;
$rows = $mdb->select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]);
if (!$rows) return [];
foreach ($rows as &$row)
$row['zip_exists'] = file_exists('../updates/' . self::zip_dir($row['version']) . '/ver_' . $row['version'] . '.zip');
return $rows;
}
public static function promote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')],
['version' => $version]
);
}
public static function demote(string $version): void
{
global $mdb;
$mdb->update('pp_update_versions',
['channel' => 'beta', 'promoted_at' => null],
['version' => $version]
);
}
public static function get_licenses(): array
{
global $mdb;
return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
}
public static function get_license(int $id): array
{
global $mdb;
return $mdb->get('pp_update_licenses', '*', ['id' => $id]) ?: [];
}
public static function save_license(array $data): void
{
global $mdb;
$row = [
'key' => trim($data['key'] ?? ''),
'domain' => trim($data['domain'] ?? ''),
'valid_to_date' => $data['valid_to_date'] ?: null,
'valid_to_version' => $data['valid_to_version'] ?: null,
'beta' => (int)(bool)($data['beta'] ?? 0),
'note' => trim($data['note'] ?? ''),
];
if (!empty($data['id']))
$mdb->update('pp_update_licenses', $row, ['id' => (int)$data['id']]);
else
$mdb->insert('pp_update_licenses', $row);
}
public static function delete_license(int $id): void
{
global $mdb;
$mdb->delete('pp_update_licenses', ['id' => $id]);
}
public static function toggle_beta(int $id): void
{
global $mdb;
$license = $mdb->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]);
if ($license)
$mdb->update('pp_update_licenses', ['beta' => $license['beta'] ? 0 : 1], ['id' => $id]);
}
private static function zip_dir(string $version): string
{
return substr($version, 0, strlen($version) - (strlen($version) == 5 ? 2 : 1)) . '0';
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/factory/class.Releases.php
```
Oczekiwane: `No syntax errors detected`
---
### Task 4: Utwórz `admin\controls\Releases`
**Files:**
- Create: `autoload/admin/controls/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\controls;
class Releases
{
public static function main_view(): string
{
return \admin\view\Releases::main_view();
}
public static function promote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::promote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function demote(): void
{
$version = trim(\S::get('version'));
if ($version)
\admin\factory\Releases::demote($version);
header('Location: /admin/releases/main_view/');
exit;
}
public static function save_license(): void
{
\admin\factory\Releases::save_license($_POST);
\S::set_message('Licencja została zapisana.');
header('Location: /admin/releases/main_view/');
exit;
}
public static function delete_license(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::delete_license($id);
header('Location: /admin/releases/main_view/');
exit;
}
public static function toggle_beta(): void
{
$id = (int)\S::get('id');
if ($id)
\admin\factory\Releases::toggle_beta($id);
header('Location: /admin/releases/main_view/');
exit;
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/controls/class.Releases.php
```
---
### Task 5: Utwórz `admin\view\Releases`
**Files:**
- Create: `autoload/admin/view/class.Releases.php`
**Step 1: Napisz klasę**
```php
<?php
namespace admin\view;
class Releases
{
public static function main_view(): string
{
$tpl = new \Tpl;
$tpl->versions = \admin\factory\Releases::get_versions();
$tpl->licenses = \admin\factory\Releases::get_licenses();
return $tpl->render('releases/main-view');
}
}
```
**Step 2: Weryfikacja składni**
```bash
php -l autoload/admin/view/class.Releases.php
```
---
### Task 6: Utwórz szablon `admin/templates/releases/main-view.php`
**Files:**
- Create: `admin/templates/releases/main-view.php`
**Step 1: Utwórz katalog i plik**
```php
<?php global $user; ?>
<div class="row">
<div class="col-lg-12">
<!-- TABS NAV -->
<ul class="nav nav-tabs releases-tabs" style="margin-bottom:20px">
<li class="active"><a href="#" data-tab="versions">Wersje</a></li>
<li><a href="#" data-tab="licenses">Licencje</a></li>
</ul>
<!-- TAB: WERSJE -->
<div id="tab-versions">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Wersje</h3></div>
<div class="panel-body">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Wersja</th><th>Kanał</th><th>Dodana</th>
<th>Promocja do stable</th><th>ZIP</th><th>Akcje</th>
</tr>
</thead>
<tbody>
<? foreach ($this->versions as $v): ?>
<tr>
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
<td>
<? if ($v['channel'] == 'stable'): ?>
<span class="label label-success">stable</span>
<? else: ?>
<span class="label label-warning">beta</span>
<? endif; ?>
</td>
<td><?= $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?></td>
<td><?= $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?></td>
<td><?= $v['zip_exists'] ? '<span class="text-success">✓</span>' : '<span class="text-danger">✗</span>' ?></td>
<td>
<? if ($v['channel'] == 'beta'): ?>
<a href="/admin/releases/promote/?version=<?= urlencode($v['version']) ?>"
class="btn btn-xs btn-success"
onclick="return confirm('Promować <?= $v['version'] ?> do stable?')">
Promuj →stable
</a>
<? else: ?>
<a href="/admin/releases/demote/?version=<?= urlencode($v['version']) ?>"
class="btn btn-xs btn-default"
onclick="return confirm('Cofnąć <?= $v['version'] ?> do beta?')">
Cofnij →beta
</a>
<? endif; ?>
</td>
</tr>
<? endforeach; ?>
<? if (!$this->versions): ?>
<tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.</td></tr>
<? endif; ?>
</tbody>
</table>
</div>
</div>
</div><!-- /tab-versions -->
<!-- TAB: LICENCJE -->
<div id="tab-licenses" style="display:none">
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center">
<h3 class="panel-title">Licencje</h3>
<button class="btn btn-sm btn-system" id="btn-add-license">+ Dodaj licencję</button>
</div>
<div class="panel-body">
<!-- Formularz dodawania/edycji (ukryty) -->
<div id="license-form-box" style="display:none;border:1px solid #ddd;padding:15px;margin-bottom:20px;background:#f9f9f9">
<h4 id="license-form-title">Nowa licencja</h4>
<form method="post" action="/admin/releases/save_license/">
<input type="hidden" name="id" id="f-id" value="">
<div class="row">
<div class="form-group col-lg-3">
<label>Klucz (hash)</label>
<input type="text" name="key" id="f-key" class="form-control" placeholder="md5/hash lub pusty dla domyślnego">
</div>
<div class="form-group col-lg-3">
<label>Domena</label>
<input type="text" name="domain" id="f-domain" class="form-control" required>
</div>
<div class="form-group col-lg-2">
<label>Ważna do daty</label>
<input type="date" name="valid_to_date" id="f-valid-date" class="form-control">
</div>
<div class="form-group col-lg-2">
<label>Ważna do wersji</label>
<input type="text" name="valid_to_version" id="f-valid-ver" class="form-control" placeholder="np. 1.618">
</div>
<div class="form-group col-lg-1">
<label>Beta</label><br>
<input type="checkbox" name="beta" id="f-beta" value="1">
</div>
<div class="form-group col-lg-2">
<label>Notatka</label>
<input type="text" name="note" id="f-note" class="form-control">
</div>
</div>
<button type="submit" class="btn btn-system btn-sm">Zapisz</button>
<button type="button" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</button>
</form>
</div><!-- /license-form-box -->
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Domena</th><th>Klucz</th><th>Do daty</th>
<th>Do wersji</th><th>Beta</th><th>Notatka</th><th>Akcje</th>
</tr>
</thead>
<tbody>
<? foreach ($this->licenses as $lic): ?>
<tr>
<td><?= htmlspecialchars($lic['domain']) ?></td>
<td><code title="<?= htmlspecialchars($lic['key']) ?>">
<?= $lic['key'] === '' ? '<em>(domyślny)</em>' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
</code></td>
<td><?= $lic['valid_to_date'] ?: '<em>∞</em>' ?></td>
<td><?= $lic['valid_to_version'] ?: '<em>∞</em>' ?></td>
<td>
<a href="/admin/releases/toggle_beta/?id=<?= $lic['id'] ?>"
class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
title="Kliknij aby przełączyć">
<?= $lic['beta'] ? 'beta' : 'stable' ?>
</a>
</td>
<td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
<td>
<button class="btn btn-xs btn-default btn-edit-license"
data-id="<?= $lic['id'] ?>"
data-key="<?= htmlspecialchars($lic['key']) ?>"
data-domain="<?= htmlspecialchars($lic['domain']) ?>"
data-valid-date="<?= $lic['valid_to_date'] ?? '' ?>"
data-valid-ver="<?= $lic['valid_to_version'] ?? '' ?>"
data-beta="<?= $lic['beta'] ?>"
data-note="<?= htmlspecialchars($lic['note'] ?? '') ?>">Edytuj</button>
<a href="/admin/releases/delete_license/?id=<?= $lic['id'] ?>"
class="btn btn-xs btn-danger"
onclick="return confirm('Usunąć licencję <?= htmlspecialchars($lic['domain']) ?>?')">Usuń</a>
</td>
</tr>
<? endforeach; ?>
<? if (!$this->licenses): ?>
<tr><td colspan="7" class="text-center text-muted">Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.</td></tr>
<? endif; ?>
</tbody>
</table>
</div>
</div>
</div><!-- /tab-licenses -->
</div>
</div>
<script>
// Przełączanie tabów
$('.releases-tabs a').on('click', function(e) {
e.preventDefault();
$('.releases-tabs li').removeClass('active');
$(this).parent().addClass('active');
$('[id^="tab-"]').hide();
$('#tab-' + $(this).data('tab')).show();
});
// Formularz: Dodaj nową licencję
$('#btn-add-license').on('click', function() {
$('#license-form-title').text('Nowa licencja');
$('#f-id').val('');
$('#f-key, #f-domain, #f-valid-date, #f-valid-ver, #f-note').val('');
$('#f-beta').prop('checked', false);
$('#license-form-box').slideDown();
});
// Formularz: Edytuj istniejącą licencję
$('.btn-edit-license').on('click', function() {
var d = $(this).data();
$('#license-form-title').text('Edytuj licencję: ' + d.domain);
$('#f-id').val(d.id);
$('#f-key').val(d.key);
$('#f-domain').val(d.domain);
$('#f-valid-date').val(d.validDate);
$('#f-valid-ver').val(d.validVer);
$('#f-beta').prop('checked', d.beta == 1);
$('#f-note').val(d.note);
$('#license-form-box').slideDown();
$('html, body').animate({ scrollTop: $('#license-form-box').offset().top - 20 }, 300);
});
// Anuluj formularz
$('#btn-cancel-license').on('click', function() {
$('#license-form-box').slideUp();
});
</script>
```
**Step 2: Zweryfikuj, że szablon jest poprawny**
Otwórz w przeglądarce: `https://cmspro.project-dc.pl/admin/releases/main_view/`
Oczekiwane: strona ładuje się bez błędów PHP, widoczne dwa taby, tabela wersji pusta lub z danymi.
---
### Task 7: Dodaj pozycję menu dla dewelopera
**Files:**
- Create: `templates/additional-menu.php` (ten plik nie trafia do klientów — dodany do `.updateignore` w Task 1)
**Step 1: Utwórz plik**
```php
<?php
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
?>
<div class="title">Developer</div>
<ul>
<li>
<a href="/admin/releases/main_view/">
<img src="/admin/css/icons/settings-20-filled.svg">Releases &amp; Licencje
</a>
</li>
</ul>
```
**Step 2: Sprawdź w przeglądarce**
Po odświeżeniu panelu admina powinna pojawić się sekcja "Developer" z linkiem "Releases & Licencje" w menu bocznym.
---
### Task 8: Migracja danych licencji do DB
Licencje z hardkodowanej tablicy `$license` w `updates/versions.php` muszą trafić do `pp_update_licenses` **przed** przejściem na nową wersję `versions.php`.
**Files:**
- Create: `_migrate_licenses.php` (tymczasowy, uruchom raz przez przeglądarkę, potem usuń)
**Step 1: Utwórz skrypt migracji**
```php
<?php
// UWAGA: Uruchom JEDEN raz przez przeglądarkę, potem natychmiast usuń!
// URL: https://cmspro.project-dc.pl/_migrate_licenses.php
require_once 'config.php';
require_once 'libraries/medoo/medoo.php';
$mdb = new medoo([
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
]);
// Wyodrębnij $license z pliku versions.php bez uruchamiania die()
$source = file_get_contents('updates/versions.php');
// Wyciągnij wszystkie wpisy $license['key']['domain'] itd.
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'domain\'\]\s*=\s*\'([^\']*)\';/', $source, $m_domain);
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_date\'\]\s*=\s*\'([^\']*)\';/', $source, $m_date);
preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_version\'\]\s*=\s*\'([^\']*)\';/', $source, $m_ver);
// Zbuduj mapę po kluczu
$dates = array_combine($m_date[1], $m_date[2]);
$vers = array_combine($m_ver[1], $m_ver[2]);
$count = 0;
foreach ($m_domain[1] as $i => $key) {
$domain = $m_domain[2][$i];
$row = [
'key' => $key,
'domain' => $domain,
'valid_to_date' => ($dates[$key] ?? '') ?: null,
'valid_to_version' => ($vers[$key] ?? '') ?: null,
'beta' => 0,
];
// Pomiń jeśli już istnieje
if ($mdb->has('pp_update_licenses', ['key' => $key])) {
echo "SKIP (już istnieje): $domain ($key)<br>";
continue;
}
$mdb->insert('pp_update_licenses', $row);
echo "OK: $domain ($key)<br>";
$count++;
}
echo "<hr><strong>Zmigrowano $count licencji.</strong>";
echo "<br><strong style='color:red'>USUŃ ten plik z serwera!</strong>";
```
**Step 2: Wgraj na serwer i uruchom**
```
https://cmspro.project-dc.pl/_migrate_licenses.php
```
Oczekiwane: lista "OK: domena (klucz)" dla każdej licencji, na końcu podsumowanie.
**Step 3: Usuń skrypt z serwera**
Skasuj `_migrate_licenses.php` — nie commituj go do repozytorium.
**Step 4: Ustaw flagę beta dla swoich testowych stron**
W panelu admina `/admin/releases/main_view/` → zakładka Licencje → przy swoich testowych domenach kliknij "stable" aby przełączyć na "beta".
---
### Task 9: Przebuduj `updates/versions.php`
**Files:**
- Modify: `updates/versions.php`
> **WAŻNE:** Wykonaj ten krok dopiero po Task 8 (migracja danych do DB).
**Step 1: Zastąp całą zawartość pliku**
```php
<?
require_once '../config.php';
require_once '../libraries/medoo/medoo.php';
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
$current_ver = 1691; // aktualizowane automatycznie przez build-update.ps1
// 1. Skan filesystem — lista istniejących ZIPów
$versions = [];
for ( $i = 1; $i <= $current_ver; $i++ )
{
$dir = substr( number_format( $i / 1000, 3 ), 0, strlen( number_format( $i / 1000, 3 ) ) - 2 ) . '0';
$version_old = number_format( $i / 1000, 2 );
$version_new = number_format( $i / 1000, 3 );
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_old . '.zip' ) )
$versions[] = $version_old;
if ( file_exists( '../updates/' . $dir . '/ver_' . $version_new . '.zip' ) )
$versions[] = $version_new;
}
$versions = array_unique( $versions );
// 2. Walidacja klucza licencji
$license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? '' ) ] );
if ( !$license )
die();
// 3. Sprawdź ważność daty
if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
die();
// 4. Auto-discovery: rejestruj nowe ZIPy jako beta
$known = array_flip( $mdb->select( 'pp_update_versions', 'version', [] ) ?: [] );
foreach ( $versions as $ver )
{
if ( !isset( $known[$ver] ) )
{
@$mdb->insert( 'pp_update_versions', [
'version' => $ver,
'channel' => 'beta',
'created_at' => date( 'Y-m-d H:i:s' )
] );
$known[$ver] = true;
}
}
// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable)
$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
// 6. Wypisz dostępne wersje
$valid_to_version = $license['valid_to_version'];
foreach ( $versions as $ver )
{
if ( !isset( $allowed[$ver] ) )
continue;
if ( $valid_to_version && $ver > $valid_to_version )
continue;
echo $ver . PHP_EOL;
}
```
**Step 2: Weryfikacja składni**
```bash
php -l updates/versions.php
```
**Step 3: Test ręczny**
Otwórz w przeglądarce (podaj klucz jednej z migrowanych licencji):
```
https://cmspro.project-dc.pl/updates/versions.php?key=TWOJ_KLUCZ
```
Oczekiwane:
- Dla klucza z `beta=0`: zwraca TYLKO wersje `channel='stable'` (pusta lista jeśli żadna jeszcze niepromowana)
- Dla klucza z `beta=1`: zwraca wersje `channel='beta'` i `'stable'`
- Dla nieprawidłowego klucza: brak odpowiedzi (die())
**Step 4: Sprawdź panel aktualizacji u klienta**
Na swojej testowej stronie (beta=1) otwórz `/admin/update/main_view/` — powinien widzieć dostępne wersje.
---
### Task 10: Commit końcowy
**Step 1: Sprawdź status**
```bash
git status
git diff updates/versions.php
```
**Step 2: Commit**
```bash
git add \
.updateignore \
autoload/admin/factory/class.Releases.php \
autoload/admin/controls/class.Releases.php \
autoload/admin/view/class.Releases.php \
admin/templates/releases/main-view.php \
templates/additional-menu.php \
updates/versions.php
git commit -m "$(cat <<'EOF'
feat: dwukanalowy system aktualizacji (beta/stable) + zarzadzanie licencjami
- Nowy modul admin\Releases: lista wersji z promocja beta→stable,
CRUD licencji z flaga beta
- versions.php czyta z DB (pp_update_licenses, pp_update_versions)
zamiast hardkodowanej tablicy $license
- Auto-discovery: nowe ZIPy automatycznie rejestrowane jako 'beta'
- Calosc wykluczona z paczek klientow przez .updateignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"
```
---
## Weryfikacja end-to-end
Po wdrożeniu sprawdź ręcznie:
1. **Nowy ZIP (beta):** Wrzuć nowy ZIP na serwer → odpytaj `versions.php` kluczem beta → pojawia się nowa wersja → w panelu admina widoczna jako `beta`
2. **Promocja:** Kliknij "Promuj →stable" → odpytaj kluczem `beta=0` → wersja pojawia się na liście
3. **Cofnięcie:** Kliknij "Cofnij →beta" → klient `beta=0` nie widzi wersji
4. **Licencja:** Dodaj nową licencję przez formularz → weryfikuj w phpMyAdmin
5. **Toggle beta:** Kliknij "stable" przy licencji → zmienia się na "beta"
6. **Zabezpieczenie:** Wgraj nową wersję CMS do klienta → sprawdź czy `admin/templates/releases/` i `autoload/admin/*/class.Releases.php` NIE znalazły się w ZIPie (build dry-run)