# 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
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
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
Wersje
Wersja Kanał Dodana
Promocja do stable ZIP Akcje
foreach ($this->versions as $v): ?>
= htmlspecialchars($v['version']) ?>
if ($v['channel'] == 'stable'): ?>
stable
else: ?>
beta
endif; ?>
= $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?>
= $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?>
= $v['zip_exists'] ? '✓ ' : '✗ ' ?>
if ($v['channel'] == 'beta'): ?>
Promuj →stable
else: ?>
Cofnij →beta
endif; ?>
endforeach; ?>
if (!$this->versions): ?>
Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.
endif; ?>
Licencje
+ Dodaj licencję
Domena Klucz Do daty
Do wersji Beta Notatka Akcje
foreach ($this->licenses as $lic): ?>
= htmlspecialchars($lic['domain']) ?>
= $lic['key'] === '' ? '(domyślny) ' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
= $lic['valid_to_date'] ?: '∞ ' ?>
= $lic['valid_to_version'] ?: '∞ ' ?>
= $lic['beta'] ? 'beta' : 'stable' ?>
= htmlspecialchars($lic['note'] ?? '') ?>
Edytuj
Usuń
endforeach; ?>
if (!$this->licenses): ?>
Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.
endif; ?>
```
**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
Developer
```
**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
'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) ";
continue;
}
$mdb->insert('pp_update_licenses', $row);
echo "OK: $domain ($key) ";
$count++;
}
echo "Zmigrowano $count licencji. ";
echo "USUŃ ten plik z serwera! ";
```
**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
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)